references/android-build-actions.md
<!-- Source: https://github.com/OutSystems/docs-odc/blob/main/src/eap/building-apps/mobile/build-actions-android.md -->
<!-- Raw (for sync): https://raw.githubusercontent.com/OutSystems/docs-odc/main/src/eap/building-apps/mobile/build-actions-android.md -->
<!-- Last verified: 2026-05-18 -->
# Android Build Actions Reference
All Android build action types supported in the ODC build actions JSON schema.
All actions go under `platforms.android` in your `buildAction.json`. The full
wrapper structure is always required:
```json
{
"platforms": {
"android": {
...actions here...
}
}
}
```
Examples in this file show only the `"android": { ... }` portion for brevity.
All actions except `appName` support an optional `condition` field for
conditional execution — see the Variables & Conditions section in SKILL.md.
---
## appName
Sets the Android app display name by updating the `label` attribute in
`AndroidManifest.xml`, or the strings resource value when a resource reference
is used in the manifest.
**Type:** `string` | **Conditional:** No
```json
"android": {
"appName": "My App Name"
}
```
---
## manifest
Modifies `AndroidManifest.xml`. Accepts an array of patch entries, each
requiring a `file` field. Exactly one operation per entry.
| Operation | Required fields | Description |
|-----------|----------------|-------------|
| `attrs` | `target` | Set or replace attributes on the target element |
| `merge` | `target` | Merge an XML string into the target (deduplication-safe) |
| `inject` | `target` | Inject an XML string into the target (allows duplicates) |
| `deleteAttributes` | `target` | Delete the listed attributes from the target element |
| `delete` | — | Delete nodes matching an XPath expression (no `target` field) |
`target` is an XPath-like path (e.g. `"manifest"`, `"manifest/application"`).
`delete` uses a full XPath expression directly (e.g. `"//intent-filter"`).
> **`merge` fragments must be rooted at the target element.** The fragment's root tag must match the `target` node — e.g. `target: "manifest"` → root is `<manifest>`, `target: "manifest/application"` → root is `<application>`. Passing a bare child element (e.g. a naked `<uses-permission />` with `target: "manifest"`) causes `xmldom` to attempt inserting it as a sibling to the document root, which is illegal XML and produces a hierarchy error at build time.
> **`attrs` values must be strings.** Boolean and number variable references are not valid in `attrs` and will fail validation. To set an attribute to a boolean or numeric value, use `inject` or `merge` with the full XML element instead.
```json
"android": {
"manifest": [
{
"file": "AndroidManifest.xml",
"target": "manifest/application",
"attrs": { "android:name": "com.example.MyApplication" }
},
{
"file": "AndroidManifest.xml",
"target": "manifest",
"merge": "<manifest>\n <uses-permission android:name=\"android.permission.CAMERA\" />\n</manifest>"
},
{
"file": "AndroidManifest.xml",
"target": "manifest/application",
"inject": "<activity android:name=\"com.example.AuthActivity\" />\n"
},
{
"file": "AndroidManifest.xml",
"target": "manifest/application",
"deleteAttributes": ["android:name"]
},
{
"file": "AndroidManifest.xml",
"delete": "//intent-filter"
}
]
}
```
---
## gradle
Modifies Gradle build files. Accepts an array of patch entries.
> **Skill maintainer context — do not relay these details to users:**
> Never generate `gradle` build actions to replicate content already present in
> the plugin's own Gradle file. A `<framework src="..." type="gradleReference">`
> element in `plugin.xml` causes Capacitor CLI to merge that file into the
> project during sync — its dependencies, repositories, and plugin declarations
> do not need a build action. Only generate `gradle` build actions for entries
> the plugin explicitly documents as app-level setup steps that go into the root
> `build.gradle` or `app/build.gradle` and are absent from the plugin's own
> file. See the `<framework>` and `<preference>` sections in
> [references/cordova-plugin-scanning.md](references/cordova-plugin-scanning.md)
> for the full decision rules.
**`insert`** — inserts new Gradle content at the target location:
- `insert` as a **string**: inserts verbatim Groovy/Gradle text
- `insert` as an **array of objects**: each object is inserted as either a
method call (`method arg`, default) or a variable assignment (`var = value`)
controlled by `insertType: "method" | "variable"` (default: `"method"`)
**`replace`** — replaces existing key-value pairs at the target location.
**`target`** mirrors the Gradle DSL hierarchy as a nested object; use `null`
as a block value (not a leaf). Set `target: null` to insert at the top level
of the file.
```json
"android": {
"gradle": [
{
"file": "build.gradle",
"target": { "buildscript": null },
"insert": [{ "classpath": "'org.javassist:javassist:3.27.0-GA'" }]
},
{
"file": "build.gradle",
"target": { "allprojects": { "repositories": null } },
"insert": [
{
"maven": [
{ "url": "https://example.com" },
{ "name": "MyFeed" }
]
}
]
},
{
"file": "variables.gradle",
"target": { "ext": null },
"insertType": "variable",
"insert": [{ "firebaseMessagingVersion": "20.0.6" }]
},
{
"file": "app/build.gradle",
"target": null,
"insert": "apply plugin: 'com.example.plugin'\n"
},
{
"file": "app/build.gradle",
"target": { "android": { "buildTypes": { "release": null } } },
"replace": { "minifyEnabled": true }
}
]
}
```
---
## res
Creates new resource files under the `res` folder of the Android project.
| Field | Required | Description |
|-------|----------|-------------|
| `path` | yes | Subfolder under `res` (e.g. `"raw"`, `"drawable"`, `"values"`) |
| `file` | yes | Output filename |
| `text` | one of | Inline file content as a string (supports `$VAR_NAME` substitution) |
| `source` | one of | Local path or URL to copy from |
```json
"android": {
"res": [
{
"path": "raw",
"file": "auth_config.json",
"text": "{\n \"client_id\": \"$CLIENT_ID\"\n}\n"
},
{
"path": "drawable",
"file": "icon.png",
"source": "../common/icon.png"
},
{
"path": "drawable",
"file": "remote-icon.png",
"source": "https://example.com/icon.png"
}
]
}
```
---
## json
Modifies the content of JSON files within the Android project.
| Operation | Description |
|-----------|-------------|
| `set` | Overrides the specified element entirely |
| `merge` | Deep-merges the provided values into the existing content |
```json
"android": {
"json": [
{
"file": "google-services.json",
"set": { "project_info": { "project_id": "MY_ID" } }
},
{
"file": "google-services.json",
"merge": { "data": { "field": "MY_FIELD" } }
}
]
}
```
---
## xml
Modifies arbitrary XML files within the Android project. Same operations as
`manifest` plus `replace`. Use `file` for project-relative paths or `resFile`
for paths relative to the `res` folder.
> **Do not use `xml` for `AndroidManifest.xml` changes — use the `manifest` action instead.** `manifest` validates attribute value types at parse time; `xml` does not, so type errors (e.g. boolean in `attrs`) will slip through validation silently and may produce incorrect output at build time.
| Operation | Required fields | Description |
|-----------|----------------|-------------|
| `attrs` | `target` | Set or replace attributes on the target element |
| `merge` | `target` | Merge XML tree (matches on attributes, appends new children) |
| `inject` | `target` | Inject XML inside the target |
| `replace` | `target` | Replace the target node with the provided XML string |
| `deleteAttributes` | `target` | Delete the listed attributes from the target element |
| `delete` | — | Delete nodes matching an XPath expression (no `target` field) |
```json
"android": {
"xml": [
{
"file": "app/network_config.xml",
"target": "network-security-config",
"merge": "<domain-config cleartextTrafficPermitted=\"true\"><domain includeSubdomains=\"true\">example.com</domain></domain-config>\n"
},
{
"resFile": "values/strings.xml",
"target": "resources/string[@name=\"app_name\"]",
"replace": "<string name=\"app_name\">My App</string>\n"
}
]
}
```
---
## copy
Copies files, directories, or URLs into the Android project. All paths are
relative to the Android project root.
> **Skill maintainer context — do not relay these details to users:**
> ODC/MABS appends a hash to resource filenames at deploy time, making
> user-supplied file paths unpredictable. Use `copy` only with hardcoded paths
> inside the plugin bundle or external URLs. If the source file is provided by
> the consuming application at runtime, a Capacitor hook is more appropriate.
| Field | Description |
|-------|-------------|
| `src` | Source path (relative to project root) or URL |
| `dest` | Destination path relative to the Android project root |
```json
"android": {
"copy": [
{
"src": "../firebase/google-services.json",
"dest": "app/google-services.json"
},
{
"src": "https://example.com/file.png",
"dest": "app/src/main/res/drawable/file.png"
}
]
}
```
---
## code
Adds source files to the project or patches existing source files. Three
variants — use exactly one per entry:
| Variant | Fields | Description |
|---------|--------|-------------|
| Copy source file | `source` + `targetDir` | Copies a source file into the specified directory |
| Replace in file | `file` + `target` + `replace` | Replaces the matched target string in the file |
| Apply patch file | `file` + `patchFile` | Applies a `.patch` file to the specified source file |
`target` in the replace variant is a string or regex pattern identifying the
text to replace.
> **Skill maintainer context — do not relay these details to users:**
>
> **Prefer other actions over `code`** — `manifest`, `gradle`, and `xml` cover
> most Android native requirements without touching source files. Only use `code`
> when there is no config-level alternative.
>
> **Avoid `patchFile`** — ODC/MABS appends a hash to deployed resource files
> (e.g., `my.patch` → `my__LoeSKZNXr0G1p13MNxJoQw.patch`), making the filename
> unpredictable and causing build failures. The `.patch` extension may also be
> unsupported in the ODC resource file list. Use `file`+`target`+`replace` for
> simple substitutions instead. For complex native code changes that cannot be
> expressed as a string replacement, a Capacitor hook is more reliable.
>
> **File paths are not searched** — the `file` field must be the full path
> relative to the Android project root (e.g.,
> `app/src/main/java/com/example/myapp/MainActivity.java`). For plugins, the
> consuming app's package name is part of the path and must be passed as a
> variable.
```json
"android": {
"code": [
{
"source": "files/MyClass.java",
"targetDir": "src/com/example"
},
{
"file": "app/src/main/java/com/example/myapp/MainActivity.java",
"target": "/import com.getcapacitor.BridgeActivity;/",
"replace": "import com.getcapacitor.BridgeActivity;\nimport com.example.MyFragment;\n"
},
{
"file": "MainActivity.java",
"patchFile": "patches/MainActivity.patch"
}
]
}
```
---
## tar
Applies tar operations on files within the Android project.
> **Skill maintainer context — do not relay these details to users:**
> ODC/MABS appends a hash to resource filenames at deploy time, making
> user-supplied file paths unpredictable. Use `tar` only when `src` is a
> hardcoded path inside the plugin bundle. If the archive is provided by the
> consuming application, a Capacitor hook is more appropriate.
| Field | Description |
|-------|-------------|
| `src` | Path to the tar file |
| `dest` | Target directory for the operation |
| `action` | Tar command: `"c"` (create), `"r"` (append), `"u"` (update), `"x"` (extract) |
```json
"android": {
"tar": [
{
"src": "files/archive.tar",
"dest": "files/extracted",
"action": "x"
}
]
}
```
references/capacitor-plugin-scanning.md
# Capacitor Plugin Scanning Guide
How to derive build actions from a Capacitor plugin's source. Used during
Generation Guidelines step 1 when `input-contract.yaml` is absent or partial.
Scan the plugin in four passes:
1. **Plugin documentation** — extract explicit native setup instructions
2. **package.json** — detect existing hooks
3. **Android native source** — confirm and supplement what documentation describes
4. **iOS native source** — confirm and supplement what documentation describes
---
## What cannot be mapped to build actions
Before scanning, identify items that are out of scope:
**Web/JavaScript code** — `src/` and `www/` contain TypeScript and JavaScript
that runs in the webview. These have no effect on the native build. Skip the
entire `src/` and `www/` trees.
**User-supplied native files** — Build actions cannot accept files as inputs
from the consuming app. If the plugin's README instructs the developer to place
a file like `GoogleService-Info.plist` or `google-services.json` into the
project, that placement cannot be performed by a build action. In ODC,
developers have no access to the native project, so the ODC-compatible approach
is for the developer to add the file as an **ODC resource** in ODC Studio
(Deploy Action: Deploy to Target Directory). Document this as an ODC setup step
in `## What requires additional setup`.
Exception: if the file is bundled inside the plugin itself (not user-supplied),
a `copy` build action can place it. See
[references/android-build-actions.md](references/android-build-actions.md) and
[references/ios-build-actions.md](references/ios-build-actions.md) for `copy`
constraints (hardcoded paths only; user-supplied paths are not reliable in ODC).
**Script-type native logic** — Hooks or setup steps that perform code
generation, SDK initialization, or branching logic beyond what `condition`
expressions support cannot be expressed as build actions. In ODC, developers
have no access to the native project, so a Capacitor hook is the only available
alternative — document these as Capacitor hooks (out of scope for this skill).
Exception: simple code insertions — adding a source file or replacing a string
in an existing one — may be expressible with the `code` build action. See
SKILL.md Generation Guidelines step 4 for constraints and limitations.
---
## Pass 1: Plugin documentation
The plugin's `README.md` (and any `docs/` directory) is the most direct signal.
Look for native setup instructions written for standard Capacitor developers who
have direct native project access — these are the primary candidates for build
actions, because in ODC those steps must be automated rather than performed
manually.
Look for these sections:
- **Android Setup / Android Configuration**
- **iOS Setup / iOS Configuration**
- **Permissions**
- **Entitlements**
- **Installation** (may include native config steps inline)
- **Gradle** or **build.gradle** configuration
- **Xcode** or **Xcode project** changes
**These signals do not need a dedicated section heading.** A single inline
sentence anywhere in the README — e.g. "configure the Privacy - Camera Usage
Description in your Info.plist" or "add the CAMERA permission to your
AndroidManifest" — is a valid plist/manifest signal and must be mapped to a
build action just as if it appeared under a dedicated setup section.
### Mapping documentation content to build actions
| README content | Build action |
|----------------|--------------|
| `AndroidManifest.xml` snippet | `manifest` (prefer `merge`) |
| Gradle dependency or plugin block | `gradle` |
| `Info.plist` key/value | `plist` |
| Entitlements entry | `entitlements` |
| Xcode framework to add | `frameworks` |
| Xcode build setting | `buildSettings` |
| Android XML resource file | `xml` |
| "Add this file to your project" (user-supplied) | Skip — document as ODC resource setup step |
For the full schema and examples of each action type, see:
[references/android-build-actions.md](references/android-build-actions.md) |
[references/ios-build-actions.md](references/ios-build-actions.md)
---
## Pass 2: package.json
### Existing Capacitor hooks
Check the `scripts` section for hook declarations following the Capacitor
lifecycle naming pattern (e.g. `after:sync`, `after:update`, `before:copy`).
Hooks that configure the native project already run during `capacitor sync` in
MABS — **no build action is needed** for the changes those hooks perform.
From each hook declaration, read the referenced script file to understand what
native changes it applies. Those changes are already covered and can be
excluded from build action generation.
**Hook migration** — only if the developer explicitly asks to migrate a hook to
a build action:
| Hook timing | Can migrate? |
|-------------|--------------|
| `after:sync` | ✅ Attempt — runs at end of sync; build actions run after sync |
| `after:update` | ✅ Attempt — same reasoning |
| `before:sync`, `before:copy`, `after:copy`, `before:update` | ❌ No — run during sync; build actions run after sync completes |
Even for migratable hooks, classify the operation first:
- Config-type (manifest patching, plist entries, Gradle changes) → map to the
appropriate build action using the same approach as Pass 3 and Pass 4
- Script-type (code generation, dependency installs, branching logic) → out of
scope; the hook must remain as-is
---
## Pass 3: Android native source
### Bundled AndroidManifest.xml
**Before writing any `manifest` build action, open
`android/src/main/AndroidManifest.xml` and read its contents.** If the entry
you are about to generate is already present there, **do not generate a build
action for it** — Capacitor CLI merges the plugin's bundled manifest into the
app manifest automatically during sync, so the entry is already covered.
Only create a `manifest` build action for entries that are:
- Required based on README instructions or source analysis but **absent** from
the bundled manifest
- Conditionally needed depending on app configuration — use a variable with a
`condition`; see [references/variables-and-conditions.md](references/variables-and-conditions.md)
Common example: many camera or barcode plugins already declare
`<uses-permission android:name="android.permission.CAMERA" />` in their
bundled `android/src/main/AndroidManifest.xml`. Do not generate a `manifest`
build action for this permission — it is already handled.
### Gradle files
The plugin's own `android/build.gradle` dependencies are applied by Capacitor
CLI during sync. Never generate `gradle` build actions to replicate content
already in the plugin's own build files. Only generate a `gradle` build action
when the plugin's README explicitly states that a change to the root or
app-level `build.gradle` is required as a setup step. Examples of such steps:
- A `maven` repository in the root `allprojects` block
- A `buildscript classpath` dependency in the root `build.gradle`
- An `apply plugin` statement in `app/build.gradle`
Check `android/build.gradle` and `android/variables.gradle` to confirm whether
the plugin already provides the entry before generating a build action for it.
**`variables.gradle` — version variable declarations:** Many plugins ship an
`android/variables.gradle` file that declares SDK version variables (e.g.
`playServicesAdsVersion = "23.0.0"`). These variables are consumed by the
plugin's own `build.gradle` at compile time. If the plugin's README instructs
the developer to set these variables in the app-level `variables.gradle`, they
require a `gradle` build action targeting `variables.gradle` with
`insertType: "variable"`. Read `android/variables.gradle` during scanning and
check the README for any instruction to set version variables at the app level.
**When to hardcode vs. expose as a variable:** Hardcode the version value from
the plugin source (e.g. `"playServicesAdsVersion": "23.0.0"`). Do not expose
internal dependency version pins as developer-facing variables unless the
plugin README explicitly presents them as developer-configurable. Surfacing them
as variables creates unnecessary ODC Studio configuration burden and invites
version mismatches.
**ODC minimum SDK constraints — do not generate build actions that violate these floors:**
- **Android `minSdkVersion`:** MABS 12+ (ODC) enforces a minimum of 28. If a
plugin's README documents a required `minSdkVersion` ≤ 28, skip the `gradle`
build action — the ODC floor already satisfies the requirement. Setting a
value below 28 will break ODC builds or cause runtime failures. Only generate
a `gradle` action for `minSdkVersion` if the required value is **greater than
28**, and include a note in the README that the app's minimum Android version
is being raised above the ODC default.
**When the README contains a developer-facing `minSdkVersion` instruction that
is suppressed by the ODC floor**, add a brief note to the generated README so
the developer is not left wondering whether they need to act. Place it in
`## What requires additional setup` with reason "Automatically satisfied by
MABS 12+" and recommended approach "No action required — MABS 12+ enforces a
minimum SDK of 28, which already meets this requirement." Example row:
| Hook / element | Reason not mapped | Recommended approach |
|----------------|-------------------|----------------------|
| `minSdkVersion = 26` (plugin README) | ODC/MABS 12+ floor (SDK 28) already satisfies this | No action required |
- **Android `compileSdkVersion` and `targetSdkVersion`:** MABS 12+ (ODC)
enforces `compileSdkVersion` 36 and `targetSdkVersion` 36. Skip any `gradle`
build action that sets these values at or below those floors — they are
already satisfied. Only generate a `gradle` action if the required value
exceeds the MABS floor.
- **iOS deployment target:** MABS 12+ (ODC) enforces a minimum deployment target
of 15. Do not generate `buildSettings` or `xcconfig` actions that set
`IPHONEOS_DEPLOYMENT_TARGET` below 15.
### Java / Kotlin source
Scan source files under `android/src/main/java/` or `android/src/main/kotlin/`:
| Signal | What to check |
|--------|---------------|
| `@CapacitorPlugin(permissions = [...])` annotation | Whether those permissions are in the bundled manifest — if yes, skip |
| `checkPermissions` / `requestPermissions` calls | Confirms runtime permissions are required |
| `import android.Manifest` | Permissions used at runtime — verify manifest coverage |
| Third-party SDK imports (e.g. `com.google.*`, `com.firebase.*`) | Gradle dependency — check if plugin's own gradle covers it or if app-level entry is needed |
| `getSystemService(Context.BLUETOOTH_SERVICE)` | Bluetooth permissions — check manifest |
| `getPackageManager().hasSystemFeature(...)` | Hardware feature declaration may be needed |
---
## Pass 4: iOS native source
### Package.swift and .podspec
Framework and library dependencies declared in `Package.swift` (`.package(url:)`)
or a `.podspec` (`s.dependency`) are handled by Capacitor CLI during sync. **No
build action is required** for these. Skip them.
### Swift / Objective-C source
Scan source files under `ios/Sources/` (SPM layout) or `ios/Plugin/` (legacy
CocoaPods layout). Framework imports and API usage are the primary signals for
`plist` usage descriptions and `entitlements` entries:
| Framework import / API usage | Plist key or entitlement needed |
|------------------------------|--------------------------------|
| `import CoreLocation` / `CLLocationManager` | `NSLocationWhenInUseUsageDescription` and/or `NSLocationAlwaysAndWhenInUseUsageDescription` |
| `import AVFoundation` / `AVCaptureDevice` | `NSCameraUsageDescription` |
| `import AVFoundation` / `AVAudioSession` | `NSMicrophoneUsageDescription` |
| `import Contacts` / `CNContactStore` | `NSContactsUsageDescription` |
| `import EventKit` / `EKEventStore` | `NSCalendarsUsageDescription` |
| `import CoreBluetooth` / `CBCentralManager` | `NSBluetoothAlwaysUsageDescription` |
| `import LocalAuthentication` / `LAContext` | `NSFaceIDUsageDescription` |
| `import Photos` / `PHPhotoLibrary` | `NSPhotoLibraryUsageDescription` and/or `NSPhotoLibraryAddUsageDescription` |
| `import CoreMotion` / `CMMotionManager` | `NSMotionUsageDescription` |
| `import CoreNFC` / `NFCReaderSession` | `NFCReaderUsageDescription` + `com.apple.developer.nfc.readersession.formats` entitlement |
| `import HealthKit` / `HKHealthStore` | `NSHealthShareUsageDescription` |
| `import UserNotifications` / `UNUserNotificationCenter` | `aps-environment` entitlement |
The framework import confirms the capability is used. Leave the usage
description text as a variable so the developer can customize it — see
[references/variables-and-conditions.md](references/variables-and-conditions.md).
Infer a sensible default from context where possible (e.g. camera plugin →
`"Used for scanning"`).
**Fixed (non-variable) plist entries:** Not every plist entry is
developer-configurable. Boolean flags and fixed identifiers required by the SDK
(e.g. `GADIsAdManagerApp: true`, `SKAdNetworkItems` with a known network ID)
must still be included as hardcoded plist entries. Do not skip them simply
because they have no variable — they are required for the SDK to function
correctly.
### Entitlements
Look for these patterns in source files or the README:
| Pattern | Entitlement |
|---------|-------------|
| `UNUserNotificationCenter`, `didRegisterForRemoteNotifications` | `aps-environment`: `"development"` or `"production"` |
| `UserDefaults(suiteName:)`, shared containers | `com.apple.security.application-groups` |
| Keychain access (`kSecAttrAccessGroup`) | `keychain-access-groups` |
| Universal links, Handoff | `com.apple.developer.associated-domains` |
| `NFCReaderSession` | `com.apple.developer.nfc.readersession.formats` |
---
## Tracking unmapped items
For every README instruction or source signal that cannot be mapped to a build
action, record:
- The item (README section, hook name, file reference)
- The reason it was not mapped (user-supplied file, script-type hook, no build
action equivalent)
- The recommended approach (Capacitor hook for script-type logic; ODC resource for user-supplied files; not supported in ODC for blockers)
This list feeds the `## What requires additional setup` section of the
generated README and the one-line terminal note. See Generation Guidelines
step 5 in SKILL.md.
---
## Summary: signal-to-action mapping
| Signal | Build action |
|--------|--------------|
| `src/` / `www/` JavaScript or TypeScript | Skip — web code, not applicable |
| README: `AndroidManifest.xml` snippet | `manifest` |
| README: Gradle dependency / plugin | `gradle` |
| README: `Info.plist` entry | `plist` |
| README: Entitlements entry | `entitlements` |
| README: Add framework in Xcode | `frameworks` |
| README: Xcode build setting | `buildSettings` |
| README: Android XML resource file | `xml` |
| README: Add file to project (user-supplied) | Skip — ODC resource setup step |
| Plugin-bundled file (not user-supplied) | `copy` — hardcoded path inside plugin bundle only |
| Existing `after:sync` / `after:update` hook (config-type) | Skip unless migration explicitly requested |
| Existing hook (script-type) | Skip — retain as Capacitor hook |
| Bundled `android/AndroidManifest.xml` entries | Skip — Capacitor CLI merges during sync |
| Plugin's own `android/build.gradle` dependencies | Skip — Capacitor CLI applies during sync |
| App-level Gradle entry (root or app `build.gradle`) | `gradle` |
| `@CapacitorPlugin(permissions = [...])` + bundled manifest | Skip — already declared |
| iOS framework import + missing plist usage description | `plist` |
| Entitlement usage pattern in source | `entitlements` |
| `Package.swift` / `.podspec` dependencies | Skip — Capacitor CLI handles during sync |
references/common-scenarios.md
# Common Scenarios
Pattern-level reference for mapping Cordova and Capacitor plugin signals to
build actions. Covers recurring patterns that may appear to be unmappable but
have a correct build action equivalent.
Before concluding that a hook or element cannot be expressed as a build action,
check this file.
---
## Pattern: Conditional plist key
### Scenario
A hook conditionally adds or omits a plist key based on a boolean preference.
The hook checks the preference and either writes the key (if `true`) or removes
it (if `false`). A common example is `NSUserTrackingUsageDescription` controlled
by an `EnableAppTrackingTransparencyPrompt` preference.
### Why this appears unmappable
`plist` build actions have no delete operation. Reading "remove
`NSUserTrackingUsageDescription` when `ENABLE_APP_TRACKING_TRANSPARENCY_PROMPT`
is `false`" can lead to the incorrect conclusion that deletion cannot be
expressed as a build action.
### Correct approach
`<config-file target="*-Info.plist">` entries are **never** written by
Capacitor CLI — the build action is the sole source of the plist key. Make the
build action conditional on the preference being `true`. When the condition is
false, the action does not run and the key is never added. No deletion is
needed.
```json
"variables": {
"ENABLE_APP_TRACKING_TRANSPARENCY_PROMPT": {
"type": "boolean",
"default": true
},
"USER_TRACKING_DESCRIPTION_IOS": {
"type": "string",
"default": "$(PRODUCT_NAME) needs your attention."
}
}
```
```json
"ios": {
"plist": [
{
"replace": false,
"condition": "eq($ENABLE_APP_TRACKING_TRANSPARENCY_PROMPT, true)",
"entries": [
{ "NSUserTrackingUsageDescription": "$USER_TRACKING_DESCRIPTION_IOS" }
]
}
]
}
```
This pattern applies to any plist key a hook conditionally sets or omits:
`NSUserTrackingUsageDescription`, permission usage descriptions, feature flags,
and any other `*-Info.plist` entry controlled by a preference.
---
## Pattern: Conditional AndroidManifest meta-data
### Scenario
A hook conditionally adds a `<meta-data>` entry to `AndroidManifest.xml` only
when a boolean preference is set to a specific value — for example, injecting
`firebase_analytics_collection_enabled = false` only when
`ANALYTICS_COLLECTION_ENABLED` is `false`.
### Why this appears unmappable
The hook has two branches: inject the entry (non-default state) or do nothing
(default state). Without a delete operation in `manifest`, it can seem like the
"do nothing" branch cannot be expressed. The correct approach makes the action
conditional so it only runs when needed.
### Two valid approaches
**Option A — Conditional injection (hardcoded value):** Only inject the entry
when the value differs from the SDK default. The injected XML hardcodes the
non-default value since the entry is only relevant in that one state.
```json
"android": {
"manifest": [
{
"file": "AndroidManifest.xml",
"condition": "eq($ANALYTICS_COLLECTION_ENABLED, false)",
"target": "manifest/application",
"merge": "<application>\n <meta-data android:name=\"firebase_analytics_collection_enabled\" android:value=\"false\" />\n</application>"
}
]
}
```
**Option B — Unconditional injection (variable value):** Always inject the
entry using the variable, explicitly declaring the state on every build
regardless of the value.
```json
"android": {
"manifest": [
{
"file": "AndroidManifest.xml",
"target": "manifest/application",
"inject": "<meta-data android:name=\"firebase_analytics_collection_enabled\" android:value=\"$ANALYTICS_COLLECTION_ENABLED\" />\n"
}
]
}
```
Both are correct. Option A relies on the SDK default covering the non-injected
case (acceptable when the SDK default matches the variable's default value).
Option B is more explicit and leaves no reliance on SDK defaults. Either is
acceptable as a build action.
---
## Pattern: Boolean preference written as a plist string
### Scenario
A plugin stores a boolean preference as a plist `<string>` entry using the
`NSString boolValue` convention — the value is `"true"` or `"false"` as a
string, parsed as a boolean at runtime. The plugin.xml comment may note this
explicitly.
### Correct approach
Use a `boolean` variable (the default value `true` / `false` makes the type
clear). Reference it with `"$X"` in the plist entry — the build actions tool
resolves the correct plist type. A single entry covers both states without
splitting into two conditional entries.
```json
"variables": {
"AUTOMATIC_SCREEN_REPORTING_ENABLED": {
"type": "boolean",
"default": true
}
}
```
```json
"ios": {
"plist": [
{
"replace": false,
"entries": [
{ "FirebaseAutomaticScreenReportingEnabled": "$AUTOMATIC_SCREEN_REPORTING_ENABLED" }
]
}
]
}
```
Do **not** split into two entries with `condition: eq($X, true)` and
`condition: eq($X, false)` — a single parameterised entry is sufficient.
---
## Pattern: Android string resources declared in plugin README
### Scenario
A Capacitor plugin documents Android configuration through a `strings.xml` snippet
in its README — for example, a notification channel name or notification color
that the native Android code reads from `res/values/strings.xml` at runtime. The
README shows the exact `<string name="...">` keys the plugin expects, and
instructs developers to add those entries to their app's `strings.xml`.
### Why this appears unmappable
There is no `res` action type. A developer reading "add this to `strings.xml`"
without knowing the correct build action type may conclude there is no way to
automate this, or may invent a non-existent action.
### Correct approach
Use the `xml` action with `resFile` pointing at the target resource file inside
the `res` folder. The `resFile` path is relative to the Android project's `res`
directory. Use `merge` targeting the parent `resources` element — this safely
appends the string entry whether or not the key already exists.
```json
"android": {
"xml": [
{
"resFile": "values/strings.xml",
"target": "resources",
"merge": "<string name=\"my_plugin_channel_name\">$NOTIFICATION_CHANNEL_NAME</string>\n"
}
]
}
```
For optional string values (e.g. a notification color that should only be set
when the developer opts in), use a boolean flag variable and a `condition`:
```json
"variables": {
"NOTIFICATION_CHANNEL_NAME": { "type": "string", "default": "My Channel" },
"ENABLE_NOTIFICATION_COLOR": { "type": "boolean", "default": false },
"NOTIFICATION_COLOR": { "type": "string", "default": "" }
}
```
```json
"android": {
"xml": [
{
"resFile": "values/strings.xml",
"target": "resources",
"merge": "<string name=\"my_plugin_channel_name\">$NOTIFICATION_CHANNEL_NAME</string>\n"
},
{
"resFile": "values/strings.xml",
"condition": "eq($ENABLE_NOTIFICATION_COLOR, true)",
"target": "resources",
"merge": "<string name=\"my_plugin_notification_color\">$NOTIFICATION_COLOR</string>\n"
}
]
}
```
The string resource key names (e.g. `my_plugin_channel_name`) must match
exactly what the plugin's native Java/Kotlin code reads via
`context.getString(R.string.my_plugin_channel_name)`. Read these from the
README's `strings.xml` snippet — do not guess or invent key names.
---
## Pattern: Local Maven repository for plugin-bundled native libraries
### Scenario
Some plugins (typically commercial or proprietary ones) ship native Android
libraries as AAR files in a `libs/` folder within the plugin bundle. Gradle must
be told where to find those files by adding a `maven { url ... }` entry to the
root `build.gradle` `allprojects.repositories` block. This requirement surfaces
in two ways:
- **Plugin README**: documents an explicit setup step instructing the developer
to add a `maven { url "${project(':capacitor-my-plugin').projectDir}/libs" }`
block to the root `build.gradle`.
- **Plugin's own `build.gradle`**: contains a `maven { url ... }` entry that
uses `${project(':capacitor-my-plugin').projectDir}` as a relative path to its
own `libs/` folder.
### Why this requires a build action
A `libs/` folder repository using `${project(':plugin-id').projectDir}` must be
declared in the root `build.gradle` `allprojects.repositories` block — the
plugin's own `android/build.gradle`, merged by Capacitor CLI during sync, is not
visible to the consuming project's dependency resolver.
### Correct approach
```json
{
"platforms": {
"android": {
"gradle": [
{
"file": "build.gradle",
"target": { "allprojects": { "repositories": null } },
"insert": [
{
"maven": [
{ "url": "\"${project(':capacitor-my-plugin').projectDir}/libs\"" }
]
}
]
}
]
}
}
}
```
The double quotes inside the `url` value are part of the Groovy string — Gradle
requires double quotes for GString interpolation. Replace `capacitor-my-plugin`
with the actual plugin project name (the Gradle project identifier, which
matches the folder name under `node_modules`).
references/cordova-plugin-scanning.md
# Cordova Plugin Scanning Guide
How to derive build actions from a Cordova plugin's source. Used during
Generation Guidelines step 1 when `input-contract.yaml` is absent or partial.
The primary source of truth is `plugin.xml`. Scan it in two passes:
1. **Declarative config elements** — determine which require build actions vs. what Capacitor CLI already handles during sync
2. **Hook elements** — classify first, then map or defer
---
## Pass 1: Declarative config elements
Build-action-relevant elements are always scoped inside a `<platform>` block:
- Elements inside `<platform name="android">` → map to **Android** build actions only
- Elements inside `<platform name="ios">` → map to **iOS** build actions only
Root-level elements (outside any `<platform>`) do not map to build actions,
with two exceptions: root-level `<hook>` elements apply to both platforms and
are classified in Pass 2; root-level `<preference>` elements may feed into
build action variables — see the `<preference>` section.
Elements not listed in this guide do not apply to build actions and can be
skipped.
### `<config-file>`
The `target` attribute identifies the file to modify; `parent` is the XPath
insertion point. Whether a build action is needed depends on both.
**What Capacitor CLI handles vs. build actions:** Entries marked
*Skip — handled by Capacitor CLI during sync* are the only cases where
Capacitor CLI writes the value automatically — no build action is needed or
appropriate. For all other rows, Capacitor CLI does not write the value; the
build action is the sole mechanism. This matters when a hook conditionally
deletes a value: if that value maps to a non-skip row, making the build action
conditional is sufficient — the key is never added if the action does not run,
so no deletion is needed. See the conditional delete pattern in Pass 2.
**Preferences in CLI-handled entries:** When a skip-row entry uses `$PREF_NAME`,
Capacitor CLI substitutes the preference's `default` value during sync. ODC
developers cannot set preference values through Capacitor CLI — the default is
always applied at sync time. Add a variable and a corresponding build action to
override the CLI-written value after sync. See the `<preference>` section for
the full variable decision logic.
| `target` value | `parent` | Build action |
|----------------|----------|--------------|
| `AndroidManifest.xml` | ends in `application` or `/*` | Skip — handled by Capacitor CLI during sync |
| `AndroidManifest.xml` | any deeper path | `manifest` (use `merge` or `inject`) |
| `*-Info.plist` | any | `plist` |
| `res/xml/*.xml` | any | `xml` with `resFile` (Android) |
| iOS entitlements file (e.g. `Entitlements-Debug.plist`) | any | `entitlements` |
| other iOS plist file (e.g. `GoogleService-Info.plist`) | any | `plist` with `file` |
| Android `res/values/*.xml` or other XML file | any | `xml` with `resFile` or `file` |
| `config.xml` | any | Skip — Cordova-specific, no build action equivalent |
| JSON file (e.g. `google-services.json`) | any | `json` build action |
| any other target | any | Skip — silently ignored by Capacitor CLI; assess case by case, no direct equivalent if not XML, plist, or JSON |
```xml
<!-- parent targets <application> directly → handled by Capacitor CLI, skip -->
<config-file target="AndroidManifest.xml" parent="/manifest/application">
<activity android:name="com.example.MyActivity" android:exported="true" />
</config-file>
<!-- parent targets manifest root → handled by Capacitor CLI, skip -->
<config-file target="AndroidManifest.xml" parent="/*">
<uses-permission android:name="android.permission.CAMERA" />
</config-file>
<!-- deeper parent path → build action needed -->
<config-file target="AndroidManifest.xml"
parent="/manifest/application/activity[@android:name='MainActivity']">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="myapp" />
</intent-filter>
</config-file>
<!-- iOS → plist entry -->
<config-file target="*-Info.plist" parent="NSCameraUsageDescription">
<string>Required for scanning.</string>
</config-file>
```
The `parent` XPath maps directly to the build action `target` field. Prefer
`merge` over `inject` in `manifest` to avoid duplicate entries.
### `<edit-config>`
A newer alternative to `<config-file>` for attribute-level changes. Processed
by Capacitor CLI using the same code paths as `<config-file>`, so the same
`file`/`target` rules apply across all targets:
| `file` value | `target` | Build action |
|--------------|----------|--------------|
| `AndroidManifest.xml` | ends in `application` or `/*` | Skip — handled by Capacitor CLI during sync |
| `AndroidManifest.xml` | any deeper path | `manifest` with `attrs` |
| `*-Info.plist` | any | `plist` |
| iOS entitlements file | any | `entitlements` |
| other iOS plist file | any | `plist` with `file` |
| `config.xml` | any | Skip — Cordova-specific, no build action equivalent |
| JSON file (e.g. `google-services.json`) | any | `json` build action |
| any other file | any | Skip — silently ignored by Capacitor CLI; assess case by case |
```xml
<!-- file targets a specific activity (deeper path) → build action needed -->
<edit-config file="AndroidManifest.xml"
target="/manifest/application/activity[@android:name='MainActivity']"
mode="merge">
<activity android:screenOrientation="portrait" />
</edit-config>
```
### `<framework>`
Fully handled by Capacitor CLI during `capacitor sync` for all relevant
variants (iOS system/custom/lib, Android plain and `gradleReference`).
Android frameworks with other `type` values (e.g. `type="system"`) are silently
ignored by Capacitor CLI and have no build action equivalent. No build action
required. Skip these elements.
**Do not generate iOS `frameworks` build actions for `<framework>` elements in
`plugin.xml`**, even when they reference well-known system frameworks such as
`AssetsLibrary.framework`, `MobileCoreServices.framework`, or
`CoreLocation.framework`. These are handled exclusively by Capacitor CLI during
sync. Generating a `frameworks` build action for them is over-generation and
will duplicate what the build pipeline already applies.
**Gradle build action scope:** The plugin's own Gradle file (applied via
`<framework type="gradleReference">`) is merged by Capacitor CLI during sync.
Never generate `gradle` build actions to replicate content that is already in
the plugin's own build files. Only generate a `gradle` build action when:
- The plugin's documentation explicitly states that a change to the root or
app-level `build.gradle` is required as a setup step, **or**
- A hook script adds something to `build.gradle` (map the hook per Pass 2).
If neither condition is met, assume the plugin's own Gradle file covers it.
### `<dependency>`
Declares a dependency on another Cordova plugin. In the standard Capacitor CLI,
`<dependency>` elements are validated only — missing dependencies are warned
about but never auto-installed. In MABS Capacitor, declared dependencies are
read and installed automatically. No build action required in either case.
Skip these elements.
### `<podspec>`
iOS only. Handled by Capacitor CLI during `capacitor sync` for CocoaPods-based
projects. For SPM-based projects, `<podspec>` is not read — the plugin requires
a `Package.swift` instead, which is outside the scope of build actions. No
build action required. Skip these elements.
### `<resource-file>` and `<lib-file>`
Fully handled by Capacitor CLI during `capacitor sync` — resource files are
copied to the appropriate native directories automatically. No build action
required. Skip these elements.
### `<source-file>` and `<header-file>`
Fully handled by Capacitor CLI during `capacitor sync` — source and header files
are copied to the appropriate native directories automatically. No build action
required. Skip these elements.
### `<preference>`
A `<preference>` declares a named value substituted as `$PREF_NAME` in other
`plugin.xml` elements. Capacitor CLI only ever uses the `default` attribute —
ODC developers cannot override preference values through Capacitor CLI.
**Step 1 — trace where `$PREF_NAME` is used.** A preference can appear in
three contexts:
- **Declarative elements** (`<config-file>`, `<framework>`, etc.) — use the
Pass 1 table to determine whether those elements produce build actions.
- **Hook scripts** — classify the hook first using Pass 2, then apply the
same variable logic if the hook maps to a build action.
- **Runtime JavaScript code** — the preference is read at app runtime, not at
build time. Not applicable for build actions; skip it.
**Step 2 — add a variable for the preference.** The default rule is: add a
variable for every preference. A variable with a `default` never causes build
failures, and it gives ODC developers the flexibility to override in ODC Studio.
The only case where a variable adds no value is when `$PREF_NAME` is used
exclusively in elements that have no direct build action equivalent whatsoever — for
example, only in `<podspec>` or `config.xml` entries. In that case, no build
action can reference the variable and it can be omitted.
For CLI-handled elements (e.g. `<config-file target="AndroidManifest.xml"
parent="/manifest/application">`): Capacitor CLI writes the value using the
preference default during sync. A build action running after sync can override
that value. Add the variable and the corresponding build action — the build
action default should match the preference default so the behaviour is unchanged
when the developer does not configure it.
**Step 3 — if one or more variables are needed, write them.** A single
`buildAction.json` can declare multiple variables, one per qualifying
preference. See
[references/variables-and-conditions.md](references/variables-and-conditions.md).
Map each `<preference name="X" default="Y">` to a variable `X`: use type
`string` by default, or infer `number`/`boolean` when the default value is
clearly numeric or boolean. Set `default` to the preference's `default`
attribute value. Reference with `$X` in build action string values.
**Boolean variables in plist entries:** A `boolean` variable can be referenced
directly as `"$X"` in a plist entry value — the build actions tool resolves the
correct plist type. A single entry such as
`{ "FirebaseAutomaticScreenReportingEnabled": "$X" }` covers both the `true`
and `false` cases without splitting into two conditional entries.
**Do not use `string` type for boolean preferences**, even if the plugin source
contains a comment about `NSString.boolValue` or stores the value as `"true"` /
`"false"` strings internally. That convention belongs to the Cordova
implementation. In build actions, always use `boolean` type — the tool resolves
the correct plist type automatically. See
[references/common-scenarios.md — "Boolean preference written as a plist string"](common-scenarios.md#pattern-boolean-preference-written-as-a-plist-string)
for the complete pattern.
```xml
<preference name="CLIENT_ID" default="" />
```
→
```json
"variables": {
"CLIENT_ID": { "type": "string", "default": "" }
}
```
### `<hook>`
Not processed in Pass 1. See **Pass 2** below for hook classification and
build action mapping.
---
## Pass 2: Hook elements
**Reading hook scripts:** Use the exact `src` path from the `<hook>` element to
locate and fetch the script — for example,
`<hook src="hooks/android/setup.js">` is at `hooks/android/setup.js` relative
to the plugin root. Do not guess alternate locations such as `scripts/`.
Build actions run **after `capacitor sync`**, during the MABS cloud build only,
and execute **once per build**. This shapes which hooks are candidates:
- Hooks that **configure the native project** (patch manifests, copy files, add
dependencies) are candidates — the config patching still needs to happen at
build time in MABS, even if it previously ran at install time in Cordova.
- Hooks that run at **deploy, emulate, run, or serve** time have no equivalent
phase in a MABS build and are not applicable.
- Hooks tied to **development workflow** (platform management, plugin
install/uninstall, clean) are not applicable.
### Hook type reference
| Hook type | Typical use | Build action suitability |
|-----------|-------------|--------------------------|
| `after_prepare` | Copy config files, patch manifests/plist after sync | ✅ Classify further |
| `before_build` | Pre-build config patching, file setup | ✅ Classify further |
| `before_compile` | Config changes before native compilation | ✅ Classify further |
| `after_plugin_install` | Post-install config setup, file copying | ✅ Classify further — patching still needed at build time |
| `before_plugin_install` | Pre-install checks, validation | ❌ No equivalent phase in MABS |
| `after_build` | Post-build tasks (archive, notify) | ❌ No post-build phase in build actions |
| `after_compile` | Post-compile tasks | ❌ Not applicable |
| `before_plugin_uninstall` | Cleanup on uninstall | ❌ Not applicable |
| `before/after_deploy` | Deploy-time tasks | ❌ Not applicable — MABS does not deploy |
| `before/after_emulate` | Emulator tasks | ❌ Not applicable |
| `before/after_run` | Device run tasks | ❌ Not applicable |
| `before/after_serve` | Dev server tasks | ❌ Not applicable |
| `before/after_clean` | Clean tasks | ❌ Not applicable |
| `before/after_platform_add/rm/ls` | Platform management | ❌ Not applicable |
| `before/after_plugin_add/rm/ls` | Plugin management | ❌ Not applicable |
### For ✅ hook types: classify the operation
Even for applicable hook types, the hook's actual operation determines the
outcome:
**Config-type operations → map to a build action:**
- Copies a bundled config file into the native project → `copy` or `res`
- Patches `AndroidManifest.xml` → `manifest`
- Patches `Info.plist` → `plist`
- Adds something to the root or app-level `build.gradle` that is not already
covered by the plugin's own Gradle file → `gradle`
- Creates or modifies an XML resource → `xml`
Use the Pass 1 element-to-action table as a guide for the specific build action
shape, and the platform reference files for the full schema and examples:
[references/android-build-actions.md](references/android-build-actions.md) |
[references/ios-build-actions.md](references/ios-build-actions.md).
**Pattern: conditional set / conditional delete**
Hooks often branch on a preference value to either set or delete a config entry.
Map these using a `condition` on the build action rather than looking for a
delete equivalent:
- **Conditional set** (`if PREF == true → set VALUE`) → build action with
`condition: eq($PREF, true)`
- **Conditional delete** (`if PREF == false → delete VALUE`) → identify where
VALUE was originally added (declarative element or another hook path). If it
comes from a `<config-file>` or `<edit-config>` element that maps to a build
action, make that build action conditional with the inverse:
`condition: ne($PREF, false)`. Because the build action is the sole source of
the value (Capacitor CLI does not auto-populate `plist` keys or deep manifest
paths), skipping it means the value is never added — no deletion is needed.
Note: `plist` has no delete operation. `manifest` and `xml` do support `delete`,
but prefer the conditional approach above when the value originates from a
declarative element — it is simpler and avoids ordering dependencies.
See [references/common-scenarios.md](references/common-scenarios.md) for concrete
JSON examples of both patterns.
**Script-type operations → out of scope (Capacitor hook territory):**
- Manages npm/pod dependencies or runs `pod install`
- Performs code generation or asset compilation
- Contains branching logic beyond what build action `condition` expressions
support
- Uses Cordova context APIs for the operation itself (plugin management,
platform manipulation) — note: using `context.opts` only to resolve the
project root, or using `ConfigParser` only to read preference values, does not
make a hook script-type if the underlying operation is config-type
The `cordova-plugin-migrator` skill classifies these hooks and determines how
they should be handled. The actual implementation — as Capacitor lifecycle hooks
(`capacitor:sync:after`, etc.) or `postinstall` npm scripts — is the
developer's responsibility and outside the scope of build actions.
**Blocker operations → document as not supported in ODC:**
- Requires user input at runtime
- Modifies `plugin.xml` at runtime
- Depends on Cordova-specific internals with no Capacitor equivalent
These cannot be expressed as build actions or Capacitor hooks without
significant rework. ODC developers have no access to the native project, so
there is no manual fallback — these scenarios represent unsupported
functionality that requires plugin redesign.
### Tracking unmapped items
For every hook or element that cannot be mapped to a build action, record:
- The hook type or element name
- The reason it was not mapped (script-type, blocker, non-applicable timing)
- The recommended approach (Capacitor hook for script-type; not supported in ODC for blockers)
This list feeds the `## What requires additional setup` section of the
generated README and the one-line terminal note. See Generation Guidelines
step 5 in SKILL.md.
---
## Summary mapping table
| `plugin.xml` element | Build action |
|----------------------|--------------|
| `<config-file target="AndroidManifest.xml">` (`parent` = `application` or `/*`) | Skip — handled by Capacitor CLI during sync |
| `<config-file target="AndroidManifest.xml">` (deeper `parent`) | `manifest` (merge or inject) |
| `<config-file target="*-Info.plist">` | `plist` |
| `<config-file target="res/xml/...">` | `xml` with `resFile` |
| `<config-file>` targeting iOS entitlements file | `entitlements` |
| `<config-file>` targeting other iOS plist file | `plist` with `file` |
| `<config-file>` targeting Android `res/values/` or other XML file | `xml` with `resFile` or `file` |
| `<config-file>` targeting a JSON file | `json` build action |
| `<config-file>` targeting any other file | Skip — silently ignored by Capacitor CLI; assess case by case |
| `<edit-config file="AndroidManifest.xml">` (`target` = `application` or `/*`) | Skip — handled by Capacitor CLI during sync |
| `<edit-config file="AndroidManifest.xml">` (deeper `target`) | `manifest` (attrs) |
| `<edit-config>` targeting `*-Info.plist` | `plist` |
| `<edit-config>` targeting iOS entitlements file | `entitlements` |
| `<edit-config>` targeting other iOS plist file | `plist` with `file` |
| `<edit-config>` targeting `config.xml` | Skip — Cordova-specific, no build action equivalent |
| `<edit-config>` targeting any other file | Skip — silently ignored by Capacitor CLI; assess case by case |
| `<framework>` (Android plain / `gradleReference`) | Skip — handled by Capacitor CLI during sync |
| `<framework>` (Android other types, e.g. `type="system"`) | Skip — silently ignored by Capacitor CLI, no build action equivalent |
| `<framework>` (iOS) | Skip — handled by Capacitor CLI during sync |
| `<dependency>` | Skip — handled by MABS Capacitor; no build action equivalent |
| `<podspec>` | Skip — handled by Capacitor CLI during sync (CocoaPods); SPM requires `Package.swift`, out of scope |
| `<resource-file>` | Skip — handled by Capacitor CLI during sync |
| `<lib-file>` | Skip — handled by Capacitor CLI during sync |
| `<source-file>` / `<header-file>` | Skip — handled by Capacitor CLI during sync |
| `<preference>` | variable — see `<preference>` section in Pass 1 for full analysis |
| `<hook>` (applicable type, config-type op) | appropriate action — see Pass 2 |
| `<hook>` (applicable type, script-type op) | out of scope → Capacitor hook |
| `<hook>` (applicable type, blocker op) | out of scope → not supported in ODC |
| `<hook>` (non-applicable type) | skip — no equivalent phase in MABS |
references/extensibility-configuration.md
<!-- Source (app schema): https://github.com/OutSystems/docs-odc/blob/main/src/eap/building-apps/mobile/extensibility-configurations/extensibility-app-reference.md -->
<!-- Source (library schema): https://github.com/OutSystems/docs-odc/blob/main/src/eap/building-apps/mobile/extensibility-configurations/extensibility-lib-reference.md -->
<!-- Last verified: 2026-05-19 -->
# ODC Extensibility Configuration
The extensibility configuration is a JSON document set in ODC Studio that wires native configuration into a mobile app or library. It is distinct from `buildAction.json` — the build action file defines *what* transformations to apply to the native project, while the extensibility configuration defines *which* files to run, *what values* to supply to their variables, and *which permissions* the plugin requires.
Extensibility configurations exist across OutSystems platforms and MABS versions. This document covers only the ODC schema with MABS 12 or later, because `buildConfigurations.buildAction` — the part that connects build action files — is only available in that context. O11 and pre-12 MABS versions use a different extensibility schema that does not support build actions or Capacitor, therefore is out of scope for this build actions skill.
## Placement in ODC Studio
| Context | Location |
|---------|----------|
| Mobile App | App > Edit app properties > **Extensibility** tab |
| Mobile Library (plugin) | Library > Edit library properties > **Extensibility** tab |
Both apps and libraries have their own extensibility configuration. They use different top-level schemas.
## Schema by context
| Context | First section | Second section |
|---------|---------------|----------------|
| App | `appConfigurations` | `buildConfigurations` |
| Library (plugin) | `pluginConfigurations` | `buildConfigurations` |
Both sections are optional. Both contexts share the same `buildConfigurations` shape.
---
## Extensibility settings
Extensibility settings are named, build-time values defined in ODC Studio and managed in ODC Portal. They are the standard mechanism for supplying variable values that would otherwise need to be hardcoded in the extensibility configuration JSON.
> **Extensibility settings vs. app settings**: ODC app settings (`Settings.<Name>`) are runtime values used in server/client actions. Extensibility settings are build-time values used only in the extensibility configuration JSON. Do not confuse the two.
### Creating an extensibility setting
In ODC Studio, open the app or library **Extensibility** tab. In the context pane:
1. Right-click **Extensibility Settings** folder → **Add Extensibility Setting**
2. Set the **Name**, **Description**, and **Data Type** (examples are Text, Boolean, Integer, Decimal, Binary)
3. In case of a sensitive value like an API Key or Token, or a file containing sensitive data, set **Is Secret** to True. This makes it so that the setting value is masked and not readable in ODC Portal. If not a sensitive value (e.g. a usage description for plist), leave it as False. Note that secret settings cannot have a default value — the developer must explicitly supply the value in ODC Portal before generating a mobile package; there is no fallback.
4. Reference the setting in the extensibility JSON as `$extensibilitySettings.SettingName`
### Setting types
| Type | Use for |
|------|---------|
| Text | String values: API keys, client IDs, URLs, usage descriptions |
| Boolean | True/false flags |
| Integer | Whole-number values: timeouts, port numbers |
| Decimal | Fractional numeric values |
| Binary | Files: `GoogleService-Info.plist`, `google-services.json`, custom certificates |
Binary settings are used as `source` values in `buildConfigurations.resources` to copy user-supplied files into the native project. All other types (text, boolean, integer, decimal) can be referenced in `parameters` via `$extensibilitySettings.SettingName`.
### Using extensibility settings to supply build action variable values
The `parameters` block accepts extensibility setting references. This is the recommended approach when the value differs between environments or should not be hardcoded in the JSON:
```json
{
"buildConfigurations": {
"buildAction": {
"config": "$resources.buildAction.json",
"parameters": {
"CLIENT_ID": "$extensibilitySettings.OAuthClientId",
"APP_SCHEME": "$extensibilitySettings.AppUrlScheme"
}
}
}
}
```
The plugin developer creates `OAuthClientId` and `AppUrlScheme` as extensibility settings in ODC Studio; the consuming app then sets their values in ODC Portal — without editing the JSON.
> Extensibility binary settings are **not** supported in `buildAction.config` or `parameters`. Use `$resources.<filename>` for the build action file reference and text extensibility settings for string parameter values.
---
## buildConfigurations
Both app and library extensibility configs share this section. It governs build-time native project configuration.
### buildAction
Connects a `buildAction.json` file to the build process and supplies values for the variables it declares. See [SKILL.md](../SKILL.md) for the full build action authoring guide.
```json
{
"buildConfigurations": {
"buildAction": {
"config": "$resources.buildAction.json",
"parameters": {
"CLIENT_ID": "com.example.myapp",
"APP_SCHEME": "myapp",
"ENABLE_DEBUG": false
}
}
}
}
```
| Field | Required | Description |
|-------|----------|-------------|
| `config` | Yes | Reference to the build action JSON file |
| `parameters` | No | Values for variables declared in the build action JSON |
#### config — referencing the build action file
`config` accepts any of the placeholder prefixes (see [Placeholder reference](#placeholder-reference)). The most common form is `"$resources.<filename>"`, which resolves to a resource added in ODC Studio with **Deploy Action** set to **Deploy to Target Directory**.
#### parameters — supplying variable values
Each key in `parameters` maps to a variable name declared in the `"variables"` block of the referenced `buildAction.json`. The value must match the declared type (`string`, `number`, or `boolean`).
```json
"parameters": {
"CLIENT_ID": "com.example.myapp",
"TIMEOUT": 30,
"ENABLE_LOGS": true
}
```
Values can also reference ODC extensibility settings, allowing the parameter to be managed outside the JSON:
```json
"parameters": {
"CLIENT_ID": "$extensibilitySettings.OAuthClientId"
}
```
**Required variables**: If a variable in `buildAction.json` declares no `default`, it must be supplied in `parameters`. If neither the library extensibility config nor the consuming app's extensibility config supplies the value, the build fails.
**Execution order**: When a library and its consuming app both define `buildConfigurations.buildAction`, the library's build action runs first, followed by the app's.
### resources
Copies files from an ODC setting, resource, or image reference into the native project at build time. This is the correct approach for developer-supplied files that build actions cannot reference (such as `GoogleService-Info.plist` or `google-services.json`).
```json
{
"buildConfigurations": {
"resources": {
"ios": [
{
"source": "$extensibilitySettings.GoogleServiceInfoPlist",
"target": "GoogleService-Info.plist"
}
],
"android": [
{
"source": "$extensibilitySettings.GoogleServicesJsonBinary",
"target": "android/app/src/main/res/google-services.json"
}
]
}
}
}
```
| Field | Required | Description |
|-------|----------|-------------|
| `source` | Yes | Source file reference (see [Placeholder reference](#placeholder-reference)) |
| `target` | Yes | Destination path relative to the native platform root |
> **Android target paths** are relative to `android/`.
> **iOS target paths** are relative to `ios/App/App/`.
>
> `resources` copying happens **before** `cap sync`. Avoid targeting paths that are overwritten by sync.
> **For build action authors**: When a plugin requires a developer-supplied config file (e.g., Firebase config), document the setup as a `buildConfigurations.resources` entry in the README rather than a `copy` build action. The `resources` mechanism uses ODC extensibility settings to accept the file from the developer — build action `copy` cannot.
---
## Declaring permissions and iOS usage descriptions
> **Skill maintainer context — do not relay these details to users:**
> This section is reference material for the generated README and for understanding the ODC extensibility ecosystem. The skill **always** generates `manifest` build actions for Android permissions and `plist` build actions for iOS usage descriptions. It never substitutes extensibility configuration JSON for build actions. References to `pluginConfigurations.permissions` in this section describe an alternative that may be mentioned in the README under specific conditions (see SKILL.md README authoring rules) — they are not generated output.
A plugin can supply Android permissions and iOS usage description strings either via build actions (the default approach for this skill — `manifest` for Android, `plist` for iOS) or via `pluginConfigurations.permissions` in the library's extensibility configuration. Both are plugin-side mechanisms. The distinction is that `pluginConfigurations.permissions` additionally allows consuming apps to override the description text — useful for generic plugins deployed across many apps where each app may want different wording. The `appConfigurations.permissions` app-side mechanism is documented below for completeness; it is not the plugin author's concern.
### Library-level: pluginConfigurations.permissions
A library declares which Android permissions and iOS protected resources the plugin needs, along with default description text. The consuming app relies on these defaults; it can override them at the app level, but that is the exception rather than the rule.
#### Android
```json
{
"pluginConfigurations": {
"permissions": {
"android": [
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO"
]
}
}
}
```
Each entry is a fully qualified Android permission string. MABS injects these into the app's `AndroidManifest.xml` at build time.
#### iOS usage descriptions
```json
{
"pluginConfigurations": {
"permissions": {
"ios": {
"NSCameraUsageDescription": {
"description": "Used for scanning barcodes."
},
"NSMicrophoneUsageDescription": {}
}
}
}
}
```
Each key is an iOS usage description key (NSXxxUsageDescription). The value is an object with an optional `description` field.
| Value | Meaning |
|-------|---------|
| `{ "description": "Some text." }` | Provides a default description; consuming app can override it |
| `{}` | Declares the key as required with no default — consuming app **must** supply a value |
**Missing description**: If a key is declared with `{}` (no default) and neither the consuming app nor any other library provides a value, the key is absent from `Info.plist`. iOS will crash the app or deny access to the protected resource at runtime when it is first requested, depending on the API and OS version. The app may also be rejected at App Store submission if the binary uses the associated API without a corresponding usage description.
#### Combined example
```json
{
"pluginConfigurations": {
"permissions": {
"android": [
"android.permission.CAMERA"
],
"ios": {
"NSCameraUsageDescription": {
"description": "Required for scanning."
}
}
}
}
}
```
### App-level: appConfigurations.permissions
Apps declare permissions directly or override values set by libraries. App-level values always take precedence over library-level values.
```json
{
"appConfigurations": {
"permissions": {
"android": [
"android.permission.CAMERA"
],
"ios": {
"NSCameraUsageDescription": "This app uses the camera to scan receipts."
}
}
}
}
```
> **Note**: iOS usage descriptions at the app level are a plain `string` (not an object). This differs from the library-level format where the value is `{ "description": "..." }`.
### Resolution order for iOS usage descriptions
| Library declares | App provides | Result |
|-----------------|--------------|--------|
| `{ "description": "Default text." }` | — | Library default is used |
| `{ "description": "Default text." }` | `"App text."` | App value overrides library |
| `{}` | `"App text."` | App value is used |
| `{}` | — | Key absent from `Info.plist` — app crashes or denies access at runtime when the protected resource is accessed; may be rejected at App Store submission |
### Common iOS usage description keys
| Key | Protected resource |
|-----|--------------------|
| `NSCameraUsageDescription` | Camera |
| `NSMicrophoneUsageDescription` | Microphone |
| `NSLocationWhenInUseUsageDescription` | Location (foreground) |
| `NSLocationAlwaysAndWhenInUseUsageDescription` | Location (background) |
| `NSBluetoothAlwaysUsageDescription` | Bluetooth LE |
| `NSFaceIDUsageDescription` | Face ID / biometrics |
| `NSContactsUsageDescription` | Contacts |
| `NSCalendarsUsageDescription` / `NSCalendarsFullAccessUsageDescription` | Calendar |
| `NSPhotoLibraryUsageDescription` | Photo library (read) |
| `NSPhotoLibraryAddUsageDescription` | Photo library (write) |
| `NFCReaderUsageDescription` | NFC |
| `NSHealthShareUsageDescription` | HealthKit (read) |
| `NSMotionUsageDescription` | Motion / accelerometer |
---
## Permissions vs. build actions
For plugin authors, both build actions and `pluginConfigurations.permissions` are plugin-side mechanisms — the plugin controls what is declared. Use the one that best fits the plugin's needs:
| Mechanism | Side | Use when |
|-----------|------|----------|
| `manifest` build action | Plugin | Android permissions — plugin controls the value directly |
| `plist` build action | Plugin | iOS usage descriptions — plugin controls the wording |
| `pluginConfigurations.permissions` | Plugin | Either platform — when apps should be able to override the description text, or when the plugin intentionally wants to require the consuming app to provide context-specific wording |
| `appConfigurations.permissions` | App | Consuming app adds or overrides permissions independently of the plugin — not relevant to plugin authors |
Build actions apply the plugin's value directly at build time. `pluginConfigurations.permissions` does the same but additionally exposes the description text for app-level override.
---
## Placeholder reference
Extensibility configuration values support these reference prefixes:
| Prefix | Resolves to |
|--------|-------------|
| `$resources.<filename>` | A resource added in ODC Studio with Deploy Action: Deploy to Target Directory |
| `$extensibilitySettings.<SettingName>` | An ODC extensibility setting (text or binary) |
| `$images.<ImageName>` | An image added to the ODC app |
---
## Cross-references
- **Build action variables** — [references/variables-and-conditions.md](variables-and-conditions.md): how variables are declared in `buildAction.json` (the plugin side of the `parameters` contract)
- **Android build actions** — [references/android-build-actions.md](android-build-actions.md): `manifest` action for permissions; `gradle` for dependencies
- **iOS build actions** — [references/ios-build-actions.md](ios-build-actions.md): `plist` for usage descriptions; `entitlements` for capabilities
references/ios-build-actions.md
<!-- Source: https://github.com/OutSystems/docs-odc/blob/main/src/eap/building-apps/mobile/build-actions-iOS.md -->
<!-- Raw (for sync): https://raw.githubusercontent.com/OutSystems/docs-odc/main/src/eap/building-apps/mobile/build-actions-iOS.md -->
<!-- Last verified: 2026-05-18 -->
# iOS Build Actions Reference
All iOS build action types supported in the ODC build actions JSON schema.
All actions go under `platforms.ios` in your `buildAction.json`. The full
wrapper structure is always required:
```json
{
"platforms": {
"ios": {
...actions here...
}
}
}
```
Examples in this file show only the `"ios": { ... }` portion for brevity.
All actions except `displayName` and `productName` support an optional
`condition` field for conditional execution — see the Variables & Conditions
section in SKILL.md.
---
## Targets and builds
iOS build actions support optional scoping by Xcode target and build
configuration. When omitted, actions apply to the default target and the
default build.
**Mutual exclusivity:** At any given nesting level, `targets`, `builds`, and
direct actions are **mutually exclusive**. If `targets` is present at a level,
all other keys at that level — including `builds` and any direct actions — are
silently dropped and never processed.
| Placement | Target | Build |
|-----------|--------|-------|
| Root `ios` level | default | default |
| Root `builds` > `"Debug"` | default | `"Debug"` |
| `targets` > `"App"` | `"App"` | default |
| `targets` > `"App"` > `builds` > `"Release"` | `"App"` | `"Release"` |
```json
"ios": {
"productName": "Applies to default target and build"
}
```
```json
"ios": {
"targets": {
"App": {
"builds": {
"Debug": { "displayName": "Debug App" },
"Release": { "displayName": "Prod App" }
}
}
}
}
```
Because `targets`, `builds`, and direct actions are mutually exclusive at each
level, the two blocks above must be expressed as separate build action entries.
Placing `productName` alongside `targets` in the same object would silently
discard `productName`.
---
## displayName
Sets the app display name shown on the device home screen.
**Type:** `string` | **Conditional:** No
```json
"ios": {
"displayName": "My App"
}
```
---
## productName
Sets the product name shown in the App Store and on the device.
**Type:** `string` | **Conditional:** No
```json
"ios": {
"productName": "My App"
}
```
---
## buildSettings
Sets Xcode build settings as key-value pairs.
**Type:** `Record<string, string>` | **Conditional:** Yes
```json
"ios": {
"buildSettings": {
"ENABLE_BITCODE": false,
"SWIFT_VERSION": "5.0"
}
}
```
---
## buildPhases
Adds or replaces custom shell script build phases in the Xcode project. By
default scripts are appended. Set `replace: true` to use `comment` as a unique
identifier and overwrite an existing build phase.
| Field | Required | Description |
|-------|----------|-------------|
| `comment` | yes | Label for the build phase; used as identifier when `replace: true` |
| `shellPath` | yes | Path to the shell (e.g. `"/bin/sh"`) |
| `shellScript` | yes | Shell script content |
| `inputPaths` | no | Array of input file paths |
| `outputPaths` | no | Array of output file paths |
| `replace` | no | If `true`, finds and replaces the existing phase with matching `comment` |
**Conditional:** Yes
```json
"ios": {
"buildPhases": [
{
"replace": true,
"comment": "Crashlytics",
"shellPath": "/bin/sh",
"shellScript": "\"${PODS_ROOT}/FirebaseCrashlytics/run\"",
"inputPaths": [
"\"$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)\""
]
}
]
}
```
---
## plist
Updates `Info.plist` (or a specified plist file) for the target and build. By
default values are merged; set `replace: true` to overwrite the entire target
object.
| Field | Required | Description |
|-------|----------|-------------|
| `entries` | yes | Array of key-value objects to add or update |
| `replace` | no | `true` overwrites existing keys; `false` (default) merges |
| `file` | no | Specific plist file to update; defaults to `Info.plist` |
**When to use `replace: true` vs `replace: false`:**
- Use `replace: true` for plugin-specific configuration keys (SDK identifiers, feature flags, App IDs) where the plugin's value must take precedence over anything the app may have set. This ensures the key is always written with the correct value on every build.
- Use `replace: false` for keys where the app's existing value should be preserved if present — typically usage description strings (`NSCameraUsageDescription`, etc.) where the app may have its own copy already set.
**Conditional:** Yes
```json
"ios": {
"plist": [
{
"replace": true,
"file": "GoogleService-Info.plist",
"entries": [{ "Key": "Value" }]
},
{
"replace": false,
"entries": [
{
"CFBundleURLTypes": [
{ "CFBundleURLSchemes": ["myapp"] }
]
},
{ "NSCameraUsageDescription": "Required for scanning." },
{ "NSFaceIDUsageDescription": "Used for authentication." }
]
}
]
}
```
### Common plist keys
| Key | Use case |
|-----|----------|
| `NSCameraUsageDescription` | Camera access |
| `NSMicrophoneUsageDescription` | Microphone access |
| `NSLocationWhenInUseUsageDescription` | Location (foreground) |
| `NSLocationAlwaysAndWhenInUseUsageDescription` | Location (background) |
| `NSBluetoothAlwaysUsageDescription` | Bluetooth LE |
| `NSFaceIDUsageDescription` | Face ID / biometrics |
| `NSContactsUsageDescription` | Contacts access |
| `NSCalendarsUsageDescription` | Calendar access |
| `CFBundleURLTypes` | Custom URL schemes |
| `LSApplicationQueriesSchemes` | Queried URL schemes |
| `UIBackgroundModes` | Background execution modes |
---
## xcprivacy
Updates the `PrivacyInfo.xcprivacy` file for the target and build. By default
values are merged; set `replace: true` to overwrite the entire target object.
| Field | Required | Description |
|-------|----------|-------------|
| `entries` | yes | Array of privacy key-value objects |
| `replace` | no | `true` overwrites; `false` (default) merges |
**Conditional:** Yes
```json
"ios": {
"xcprivacy": [
{
"replace": true,
"entries": [{ "NSPrivacyTracking": [] }]
},
{
"replace": false,
"entries": [
{
"NSPrivacyAccessedAPITypes": {
"NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults",
"NSPrivacyAccessedAPITypeReasons": ["CA92.1"]
}
}
]
}
]
}
```
---
## entitlements
Updates the `.entitlements` file for the target and build. This is an **object**
(not an array). By default values are merged; set `replace: true` to overwrite
the entire target object.
| Field | Required | Description |
|-------|----------|-------------|
| `entries` | yes | Array of entitlement key-value objects |
| `replace` | no | `true` overwrites; `false` (default) merges |
**Conditional:** Yes
```json
"ios": {
"entitlements": {
"replace": false,
"entries": [
{ "aps-environment": "production" },
{ "keychain-access-groups": ["$(AppIdentifierPrefix)com.example.app"] },
{ "com.apple.security.application-groups": ["group.com.example.app"] }
]
}
}
```
### Common entitlement keys
| Key | Use case |
|-----|----------|
| `aps-environment` | Push notifications (`"development"` or `"production"`) |
| `com.apple.security.application-groups` | Shared data between app and extensions |
| `keychain-access-groups` | Shared keychain between apps |
| `com.apple.developer.associated-domains` | Universal links, Handoff |
| `com.apple.developer.nfc.readersession.formats` | NFC reading |
| `com.apple.developer.siri` | SiriKit integration |
---
## frameworks
Adds frameworks to the Xcode project.
| Field | Required | Description |
|-------|----------|-------------|
| `name` | yes | Framework name (e.g. `"AudioToolbox.framework"`) |
| `customFramework` | no | Whether this is a custom (non-system) framework |
| `link` | no | Whether to link the framework |
| `embed` | no | Whether to embed the framework |
**Conditional:** Yes
```json
"ios": {
"frameworks": [
{ "name": "AudioToolbox.framework" },
{ "name": "CoreServices.framework" },
{ "name": "MyCustom.framework", "customFramework": true, "embed": true }
]
}
```
---
## json
Modifies the content of JSON files within the iOS project. `file` is relative
to the iOS project root.
| Operation | Description |
|-----------|-------------|
| `set` | Overrides the specified element entirely |
| `merge` | Deep-merges the provided values into existing content |
**Conditional:** Yes
```json
"ios": {
"json": [
{
"file": "google-services.json",
"set": { "project_info": { "project_id": "MY_ID" } }
},
{
"file": "google-services.json",
"merge": { "data": { "field": "MY_FIELD" } }
}
]
}
```
---
## xml
Modifies arbitrary XML files within the iOS project.
| Operation | Required fields | Description |
|-----------|----------------|-------------|
| `attrs` | `target` | Set or replace attributes on the target element |
| `merge` | `target` | Merge XML tree (matches on attributes, appends new children) |
| `inject` | `target` | Inject XML inside the target |
| `replace` | `target` | Replace the target node with the provided XML string |
| `deleteAttributes` | `target` | Delete the listed attributes from the target element |
| `delete` | — | Delete nodes matching an XPath expression (no `target` field) |
**Conditional:** Yes
```json
"ios": {
"xml": [
{
"file": "App/config.xml",
"target": "entries/field",
"merge": "<field>\n <string>Value</string>\n</field>\n"
}
]
}
```
---
## copy
Copies files, directories, or URLs into the iOS project. Paths are relative to
the iOS project root.
> **Skill maintainer context — do not relay these details to users:**
> ODC/MABS appends a hash to resource filenames at deploy time, making
> user-supplied file paths unpredictable. Use `copy` only with hardcoded paths
> inside the plugin bundle or external URLs. If the source file is provided by
> the consuming application at runtime, a Capacitor hook is more appropriate.
| Field | Description |
|-------|-------------|
| `src` | Source path (relative to project root) or URL |
| `dest` | Destination path relative to the iOS project root |
**Conditional:** Yes
```json
"ios": {
"copy": [
{
"src": "../firebase/GoogleService-Info.plist",
"dest": "App/GoogleService-Info.plist"
},
{
"src": "https://example.com/file.png",
"dest": "App/Assets/file.png"
}
]
}
```
---
## strings
Updates `.strings` files for localization. Use `set` for inline key-value pairs
or `setFromJson` to load values from a JSON file.
| Field | Required | Description |
|-------|----------|-------------|
| `file` | yes | Path to the `.strings` file |
| `set` | one of | Object of key-value string pairs to add or update |
| `setFromJson` | one of | Path to a JSON file whose key-value pairs are applied |
**Conditional:** Yes
```json
"ios": {
"strings": [
{
"file": "App/Localizable.strings",
"set": { "Insert Element": "Insert Element" }
},
{
"file": "App/Localizable.strings",
"setFromJson": "lang/en.json"
}
]
}
```
---
## xcconfig
Updates `.xcconfig` files with build configuration key-value pairs.
| Field | Required | Description |
|-------|----------|-------------|
| `file` | yes | Path to the `.xcconfig` file |
| `set` | yes | Object of key-value pairs to add or update |
**Conditional:** Yes
```json
"ios": {
"xcconfig": [
{
"file": "App/Config.xcconfig",
"set": { "PRODUCT_NAME": "$NAME" }
}
]
}
```
---
## code
Adds source files to the project or patches existing source files. Three
variants — use exactly one per entry. Note: iOS and Android `code` action
shapes differ — iOS `source` does not require `targetDir`.
| Variant | Fields | Description |
|---------|--------|-------------|
| Add source file | `source` + optional `compilerFlags` | Adds a source file to the Xcode project |
| Replace in file | `file` + `target` + `replace` | Replaces the matched target string in the file |
| Apply patch file | `file` + `patchFile` | Applies a `.patch` file to the specified source file |
> **Skill maintainer context — do not relay these details to users:**
>
> **Prefer other actions over `code`** — `plist`, `entitlements`, `buildSettings`,
> `buildPhases`, and `xcconfig` cover most iOS native requirements without
> touching source files. Only use `code` when there is no config-level
> alternative.
>
> **Avoid `patchFile`** — ODC/MABS appends a hash to deployed resource files
> (e.g., `my.patch` → `my__LoeSKZNXr0G1p13MNxJoQw.patch`), making the filename
> unpredictable and causing build failures. The `.patch` extension may also be
> unsupported in the ODC resource file list. Use `file`+`target`+`replace` for
> simple substitutions instead. For complex native code changes that cannot be
> expressed as a string replacement, a Capacitor hook is more reliable.
>
> **File paths are not searched** — the `file` field must be the full path
> relative to the iOS project root (e.g., `App/AppDelegate.swift`).
**Conditional:** Yes
```json
"ios": {
"code": [
{
"source": "files/CustomBridge.swift"
},
{
"source": "files/FooBarLib.a",
"compilerFlags": "-fno-objc-arc"
},
{
"file": "App/AppDelegate.swift",
"target": "/import Capacitor/",
"replace": "import Capacitor\nimport WatchConnectivity\n"
},
{
"file": "App/AppDelegate.swift",
"patchFile": "patches/ChangeAppDelegate.patch"
}
]
}
```
---
## tar
Applies tar operations on files within the iOS project.
> **Skill maintainer context — do not relay these details to users:**
> ODC/MABS appends a hash to resource filenames at deploy time, making
> user-supplied file paths unpredictable. Use `tar` only when `src` is a
> hardcoded path inside the plugin bundle. If the archive is provided by the
> consuming application, a Capacitor hook is more appropriate.
| Field | Description |
|-------|-------------|
| `src` | Path to the tar file |
| `dest` | Target directory for the operation |
| `action` | Tar command: `"c"` (create), `"r"` (append), `"u"` (update), `"x"` (extract) |
**Conditional:** Yes
```json
"ios": {
"tar": [
{
"src": "files/FooBar.tar",
"dest": "files/FooBar",
"action": "x"
}
]
}
```
references/readme-template.md
# README Template for Build Actions
Use this structure when generating `build-actions/README.md`. Follow the
authoring rules in SKILL.md (Generation Guideline 5) to determine which
sections to include or omit.
---
```markdown
# <Plugin/App Name> Build Actions
<One short paragraph: what this plugin/app does and why native build
configuration is required — inferred from the generated actions.>
## What this configures
### Android
| Action | Purpose |
|--------|---------|
| `<action type>` | <what it sets up> |
### iOS
| Action | Purpose |
|--------|---------|
| `<action type>` | <what it sets up> |
## What requires additional setup
| Hook / element | Reason not mapped | Recommended approach |
|----------------|-------------------|----------------------|
| `<hook type>` | <why it can't be a build action> | Capacitor hook |
## Variables
| Variable | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| `VAR_NAME` | string | yes | — | What this value controls |
## ODC Setup
1. In ODC Studio, add `buildAction.json` as a resource and set **Deploy Action**
to **Deploy to Target Directory**.
2. Configure extensibility to reference the file and resolve its variables.
The path depends on the target:
- **ODC app:** App > Edit app properties > Extensibility
- **ODC Mobile Library (plugin):** Library > Edit library properties > Extensibility
```json
{
"buildConfigurations": {
"buildAction": {
"config": "$resources.buildAction.json",
"parameters": {
"VAR_NAME": "value"
}
}
}
}
```
Values in `parameters` can be hardcoded literals or extensibility setting references
(`$extensibilitySettings.SettingName`). Use extensibility settings for any value that
consuming apps should be able to configure. The plugin developer creates the settings
in ODC Studio: right-click **Extensibility Settings** in the context pane →
**Add Extensibility Setting**. For sensitive values (like API keys or tokens), set
**Is Secret** to True — secret settings have no default and must be supplied in ODC
Portal before generating a mobile package. The consuming app then sets values in
ODC Portal → app → **Mobile distribution** → **Extensibility settings**.
3. Build in the ODC Portal using MABS 12 or greater:
- **ODC app:** build the app directly.
- **ODC Mobile Library (plugin):** consume the library in an ODC app, then
build that app.
```
references/variables-and-conditions.md
# Variables & Conditions
## Variables
Variables are optional inputs that developers can set from ODC Studio. They
allow the same build action to behave differently across apps.
```json
"variables": {
"APP_NAME": {
"type": "string",
"default": ""
},
"TIMEOUT": {
"type": "number",
"default": 30
},
"ENABLE_DEBUG": {
"type": "boolean",
"default": false
}
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | `"string"`, `"number"`, or `"boolean"` |
| `default` | any | no | Fallback value used when the developer does not set the variable. Recommended in most cases so the build action has sensible out-of-the-box behavior. Without a default, the developer must supply a value or the build will fail. |
**Usage in values:** Reference variables with `$VAR_NAME` anywhere in string
values inside the JSON.
```json
"attrs": { "android:name": "com.example.$APP_NAME" }
```
## Conditions
Conditions control whether an individual action runs. Add a `condition` field
to any action entry (except `displayName`, `productName`, and `appName` — see
platform reference files) using function-style expressions.
**`condition` must be a string.** Never use an object, array, or any other type — the validator will reject it. The only valid form is the function-style string syntax shown below.
These are all **invalid** and will fail validation:
```json
"condition": { "operator": "ne", "left": "$COLOR", "right": "" }
"condition": { "op": "ne", "arg1": "$COLOR", "arg2": "" }
"condition": ["ne", "$COLOR", ""]
```
This is the **only valid form**:
```json
"condition": "ne($COLOR, red)"
```
> **Do not quote string literals in conditions.** The build system performs variable interpolation before parsing the condition — `$VAR` is replaced with its raw value first, then the expression is parsed. Any surrounding quote characters you write become part of the parsed argument string. For example, `eq($MODE, "prod")` after interpolation becomes `eq(prod, "prod")` — the parser sees `args[1]` as `"prod"` (with literal quote characters), which never equals `prod`, so the condition is always false. Write bare values: `eq($MODE, prod)`.
| Operator | Meaning | Example |
|----------|---------|---------|
| `eq(a, b)` | equal | `eq($MODE, prod)` |
| `ne(a, b)` | not equal | `ne($ENV, dev)` |
| `gt(a, b)` | greater than | `gt($VERSION, 10)` |
| `ge(a, b)` | greater than or equal | `ge($COUNT, 0)` |
| `lt(a, b)` | less than | `lt($TIMEOUT, 60)` |
| `le(a, b)` | less than or equal | `le($LEVEL, 5)` |
Arguments can be variable references (`$VAR_NAME`) or literal values:
```json
{
"file": "AndroidManifest.xml",
"condition": "ge($EXAMPLE_NUMBER, 0)",
"target": "manifest/application",
"attrs": { "android:name": "com.example.$APP_NAME" }
}
```
### Limitation: empty string comparisons are not supported
Conditions cannot compare a variable against an empty string literal (`''`). The following is **invalid** and will fail validation:
```json
"condition": "ne($SOME_STRING, '')"
```
This is only a problem when a string variable has an empty-string default — if the default is a meaningful value, the condition is unnecessary entirely.
**Correct pattern — use a boolean flag instead.**
When the intent is "apply this action only if the user provided a value", add a companion boolean variable and condition on that:
```json
"variables": {
"ENABLE_NOTIFICATION_COLOR": {
"type": "boolean",
"default": false
},
"NOTIFICATION_COLOR": {
"type": "string",
"default": ""
}
}
```
```json
{
"condition": "eq($ENABLE_NOTIFICATION_COLOR, true)",
"resFile": "values/strings.xml",
"target": "resources/string[@name=\"notification_color\"]",
"replace": "<string name=\"notification_color\">$NOTIFICATION_COLOR</string>\n"
}
```
The developer sets `ENABLE_NOTIFICATION_COLOR` to `true` in ODC Studio when they also supply a value for `NOTIFICATION_COLOR`. When `ENABLE_NOTIFICATION_COLOR` is `false` (the default), the action is skipped entirely.
---
## See also: supplying variable values in ODC
Variables declared here are supplied at build time via the `parameters` block in the extensibility configuration. Values in `parameters` can be hardcoded literals or extensibility setting references (`$extensibilitySettings.SettingName`). The plugin developer creates extensibility settings in ODC Studio and references them in `parameters`; the consuming app then sets their values in ODC Portal — without hardcoding anything in the JSON.
See **[references/extensibility-configuration.md](extensibility-configuration.md)** for the `parameters` contract and how to create extensibility settings in ODC Studio.
SKILL.md
---
name: build-actions-generator
description: >-
Generates OutSystems Developer Cloud (ODC) build action JSON files that
configure Capacitor mobile plugin builds for Android and iOS. Produces
correct JSON with platform-specific actions, input variables, and conditional
logic. Use when a developer says "create a build action for my ODC plugin",
"generate a buildAction.json file", "set up Gradle or plist build actions for
a Capacitor or Cordova plugin", "configure AndroidManifest for ODC build", or
"scaffold ODC native build configuration". If the target platform is
ambiguous, still generate and note that build actions only apply to ODC —
they have no effect in standalone Capacitor apps. Do not use when the
developer explicitly mentions Cordova apps, O11, Cordova extensibility
configurations, MABS versions prior to 12, or uploading and registering the
JSON in ODC Studio.
metadata:
author: ionic
source: https://github.com/ionic-team/capacitor-skills
---
# ODC Build Actions Generator
Generates `buildAction.json` for ODC Mobile Libraries (Capacitor and Cordova plugins) and ODC apps targeting Android and iOS via MABS 12+.
## When to Use
✅ **Use this skill when:**
- Generating a `buildAction.json` file for an ODC Mobile Library (Capacitor or Cordova plugin).
- Generating a `buildAction.json` file for an ODC app that requires native build configuration.
- Generating build actions for a Cordova plugin being adapted for ODC (Capacitor-based) deployment.
- Configuring `AndroidManifest.xml`, Gradle files, or XML resources for Android.
- Configuring `Info.plist`, entitlements, or display name for iOS.
- Defining input variables (string, number, boolean) and conditional logic.
- Scaffolding build actions for both platforms from a plugin's native requirements.
- Invoked with a plugin path argument or from within a Capacitor or Cordova plugin directory.
❌ **Do NOT use this skill for:**
- Uploading or referencing the JSON in ODC Studio/Portal (Steps 2–3 — see manual guidance).
- Cordova extensibility configurations or O11 extensibility JSON.
- Cordova app builds, O11 builds, or MABS versions prior to 12 — build actions only apply to Capacitor apps on ODC (MABS 12+).
- App-level build configuration that lives outside the build action JSON.
- Publishing or testing the plugin (Step 4 — developer responsibility).
---
## End-to-End Process
This skill handles **Step 1 only**. Steps 2–4 require manual action.
| Step | Owner | What |
|------|-------|------|
| **1. Generate JSON** | This skill | Create `buildAction.json` with all required platform actions |
| **2. Upload JSON** | Developer | Add the file to the plugin/library in ODC Studio → Resources, and then reference it Extensibility tab, creating Extensibility Settings for any variables. |
| **3. Link in Portal** | Developer | If the build action has parameters, provide values in ODC Portal → Mobile Distribution tab → Extensibility settings |
| **4. Publish & test** | Developer | Test a mobile build using MABS 12 (Capacitor) or later |
---
## JSON File Structure
**Output location:** Two files are written to the `build-actions/` folder at
the plugin root:
- `build-actions/buildAction.json` — the build action configuration
- `build-actions/README.md` — human-readable documentation (see Generation Guidelines step 5)
**Always create `build-actions/` at the top level of the directory provided
(or the current working directory if no path argument was given).** Never
create it inside a subdirectory such as `plugin/`, `android/`, or `ios/`,
even when scanning source files that live in those subdirectories. If the
plugin repo has a nested structure (e.g. `plugin/android/`, `plugin/ios/`),
the output still goes at the repo root: `build-actions/buildAction.json`.
```json
{
"variables": { }, // optional — input parameters for the build action
"platforms": {
"android": { }, // optional — Android-specific actions
"ios": { } // optional — iOS-specific actions
}
}
```
**File naming:** Use camelCase without spaces.
✅ `buildAction.json`, `pushNotifications.json`, `cameraPlugin.json`
❌ `build action.json`, `Build_Action.json`
At least one of `android` or `ios` must be present under `platforms`.
---
## Variables & Conditions
If the build action uses variables or conditions, read **`references/variables-and-conditions.md`** for full syntax and examples.
- **Variables** (`"variables"` key) — typed inputs (`string`, `number`, `boolean`) declared by the plugin developer; consuming apps supply values via the extensibility configuration `parameters` block. Always include a `default` unless the value is genuinely required; without one the build fails if unset.
- **Platform-specific variants** — if the same logical value (e.g. an App ID, API key) is used on both Android and iOS but is typically distinct per platform, expose **separate variables** with `_ANDROID` and `_IOS` suffixes (e.g. `ADMOB_APP_ID_ANDROID`, `ADMOB_APP_ID_IOS`). Do not merge them into a single shared variable — the developer must be able to configure each platform independently.
- **Conditions** — add a `condition` field to any action entry (except `displayName`, `productName`, and `appName`) to conditionally skip it; for syntax and operators, see the reference file.
> If variables are defined, read **`references/extensibility-configuration.md`** for how `parameters` in the extensibility configuration supplies values for variables declared in `buildAction.json`. When a library and its consuming app both define build actions, the library's runs first.
---
## Android Actions
If targeting Android, read **`references/android-build-actions.md`** for full action schemas and examples.
Available actions:
- `appName` — Set the Android app name (string, no condition support)
- `manifest` — Modify `AndroidManifest.xml` (set attributes, merge or inject XML)
- `gradle` — Patch Gradle build files (insert or replace at target DSL path)
- `res` — Create resource files under the `res/` folder
- `json` — Modify JSON files (`set` or `merge`)
- `xml` — Modify arbitrary XML resource files
- `copy` — Copy files, directories, or URLs into the project
- `code` — Add or patch native Android (Java/Kotlin) source files (`source`+`targetDir`, `file`+`target`+`replace`, or `file`+`patchFile`)
- `tar` — Apply tar operations on project files
---
## iOS Actions
If targeting iOS, read **`references/ios-build-actions.md`** for full action schemas and examples.
Available actions:
- `displayName` — Set app display name shown on the home screen (no condition support)
- `productName` — Set product name shown in App Store (no condition support)
- `buildSettings` — Set Xcode build settings as key-value pairs
- `buildPhases` — Add or replace custom shell script build phases
- `plist` — Modify `Info.plist` or other plist files (replace or merge entries)
- `xcprivacy` — Update `PrivacyInfo.xcprivacy`
- `entitlements` — Add or modify entitlements (**object**, not array)
- `frameworks` — Add system or custom frameworks to the Xcode project
- `json` — Modify JSON files (`set` or `merge`)
- `xml` — Modify arbitrary XML files
- `copy` — Copy files, directories, or URLs into the project
- `strings` — Update `.strings` localization files
- `xcconfig` — Update `.xcconfig` build configuration files
- `code` — Add or patch native iOS (Swift/Objective-C) source files (`source`+`compilerFlags`, `file`+`target`+`replace`, or `file`+`patchFile`)
- `tar` — Apply tar operations on project files
---
## Generation Guidelines
### 1. Read plugin input signals
Plugin root is the path argument if one was given, otherwise the current directory. Before asking the developer any questions, check for existing signals in the plugin:
- If `input-contract.yaml` exists at the plugin root, read it against the full
`capacitor-plugin-generator/references/input-contract.md` schema and extract
the following relevant sections:
- `migration.hooks` (`tier_1` / `tier_2` / `tier_3`) — hook classification
for build-action derivation.
- `dependencies.android.gradle` and
`dependencies.ios.{cocoapods,spm,system_frameworks}` — dependency-based
actions (Gradle patches, framework entries).
- `permissions.android` and `permissions.ios` — permission actions
(manifest `<uses-permission>`, plist usage descriptions).
- `plugin.name` — for the README title.
- All other fields (`api.methods`, `api.types`, `api.events`, etc.) are
irrelevant — ignore silently.
- **Always also scan plugin source directly**, even when the contract is
present. The contract is a starting point; source scanning catches signals
the contract may not capture or may be stale on:
- **Cordova plugins:** parse `plugin.xml` — read **`references/cordova-plugin-scanning.md`** for the full element-to-action mapping and hook classification guide.
- **Capacitor plugins:** scan plugin documentation, `package.json`, and native source files (Java/Kotlin and Swift/Objective-C) — read **`references/capacitor-plugin-scanning.md`** for the full scanning guide.
- When invoked as part of the `cordova-plugin-migrator` ODC flow (Phase 11a),
the output (`build-actions/`) is written to the **Capacitor plugin
directory**, not the Cordova source tree. The `cordova-plugin-migrator`
supplies the Capacitor plugin path as the working directory or argument.
### 2. Gather requirements
Ask the developer (or infer from context):
- What native capabilities does the plugin need? (camera, location, push, Bluetooth, etc.)
- Which platforms are targeted: Android only, iOS only, or both?
- Are there runtime configuration values the developer should control? (→ variables)
- Are any actions conditional on those values?
### 3. Map requirements to actions
| Native requirement | Android action | iOS action |
|--------------------|----------------|------------|
| Runtime permission | `manifest` inject `<uses-permission>` | `plist` usage description key |
| Custom URL scheme | `manifest` merge intent-filter | `plist` `CFBundleURLTypes` |
| Custom app attribute | `manifest` attrs | — |
| Native dependency | `gradle` replace | — |
| Push notifications | `manifest` merge + `gradle` | `entitlements` `aps-environment` |
| App groups | — | `entitlements` `com.apple.security.application-groups` |
| Custom display name | — | `displayName` |
| Custom native code | `code` inject/replace | `code` inject/replace |
> If a hook or element does not map clearly to any action listed above, read **`references/common-scenarios.md`** for patterns that appear unmappable but have correct build action equivalents.
### 4. Generate the JSON
- Filename: camelCase, no spaces (e.g., `buildAction.json`)
- Only include platforms that have actual actions
- Use `$VAR_NAME` substitution for developer-controlled values
- Include a `default` on variables unless the value is genuinely required — without one, the build fails if neither the plugin library nor the consuming app supplies the variable value in the extensibility configuration `parameters`
- Add `condition` only when an action should be conditionally skipped
- Prefer `merge` over `inject` in `manifest` to avoid duplicate entries
- Output valid, well-formatted JSON
- The `code` action has **platform-specific file path conventions** — iOS targets
Swift/Objective-C files (e.g., `App/AppDelegate.swift`); Android targets
Java/Kotlin files (e.g., `app/src/main/java/com/example/App.java`). Do not
mix conventions between platforms.
- **Prefer config-level actions over `code`** — `manifest`, `gradle`, `plist`,
`xml`, and `entitlements` cover most native requirements without touching
source files. Only generate a `code` action when no config-level alternative
exists. If you do, use `file`+`target`+`replace`; never use `patchFile` — it
is unreliable in ODC builds. If the change cannot be expressed as a simple
string replacement, briefly tell the user the approach is not reliable in ODC
and suggest a Capacitor hook as the alternative (out of scope for this skill).
Do not explain ODC internals.
- **Avoid `tar` and `copy` when the source file is user-supplied at runtime** —
these actions are only reliable with hardcoded paths inside the plugin bundle
or external URLs. If the use case requires user-provided files, briefly tell
the user to consider a Capacitor hook instead. Do not explain ODC internals.
After writing the JSON file, validate its syntax before proceeding:
1. Tell the user: *"Validating JSON syntax..."*
2. Try `python3 -m json.tool build-actions/buildAction.json > /dev/null` — use
this if `python3` is available.
3. If `python3` is not available, try `jq empty build-actions/buildAction.json`.
4. If neither tool is available, self-inspect the file carefully: check for
balanced braces and brackets, no trailing commas, and correct escaping in
string values (pay special attention to XML content inside `manifest`/`plist`
entries where quotes must be escaped as `\"`).
5. If an error is found (by tool or self-inspection), fix the file and repeat
from step 2 until the JSON is valid.
6. Only proceed to the generation guideline 5 (README) once validation passes.
### 5. Generate the README and wrap up
Generate `build-actions/README.md` alongside the JSON. It serves as
source-control documentation for the plugin and as the primary reference for
the developer setting up ODC. Read **`references/readme-template.md`** for
the required structure, then follow the authoring rules below.
**README authoring rules:**
- Omit the `### Android` or `### iOS` section if that platform has no actions.
- Omit `## What requires additional setup` entirely if all input signals were
mapped to build actions. Include it only when hooks or elements were found
that could not be mapped.
- Omit the `## Variables` section entirely if there are no variables.
- Omit the extensibility settings instructions (the paragraph below the JSON block in step 2 of `## ODC Setup`) if the plugin has no variables — the `parameters` key is absent in that case and the instructions have no context.
- In the `## Variables` table, set Required to `yes` if there is no default,
`no` if a default exists. Leave Default as `—` when Required is `yes`.
- The extensibility JSON in `## ODC Setup` should reflect actual variable names
from the generated JSON, not placeholder `VAR_NAME`.
- In `## What requires additional setup`, set Recommended approach to:
- `Capacitor hook` for script-type hooks — describe concretely what the hook must do
- `ODC resource` for user-supplied files — tell the developer to add the file as an ODC resource in ODC Studio (Deploy Action: Deploy to Target Directory)
- `Not supported in ODC` for blockers — ODC developers have no access to the native project, so there is no manual fallback; briefly state why rework would be needed
- **Extensibility config permissions alternative**: Only mention `pluginConfigurations.permissions` in the README if (a) the developer explicitly asked about extensibility configurations, or (b) every generated build action is exclusively a `manifest` `<uses-permission>` entry and/or a `plist` iOS usage description entry — meaning the entire `buildAction.json` could be replaced by the `permissions` block in the library extensibility configuration. In all other cases omit it; the reference is in [references/extensibility-configuration.md](references/extensibility-configuration.md).
**Terminal output after generating both files:**
- Do not output the `buildAction.json` contents — the file write already
displays them.
- Do not show analysis tables, scanning decisions, or per-element reasoning in
the terminal. Internal reasoning stays internal; noteworthy decisions that did
not map cleanly belong in the README, not the terminal.
- Output a single short note: *"Build actions written to `build-actions/`. This is a candidate — review `build-actions/buildAction.json` before use, then validate with a MABS 12+ build and functional tests against a real mobile app. See `build-actions/README.md` for a summary of what this configures and ODC setup instructions."*
- If `## What requires additional setup` was written to the README, add one
additional line: *"Some hooks or elements could not be mapped to build
actions — see `build-actions/README.md` for details."*
- If the developer did not explicitly mention ODC as the target platform, add
one sentence noting that build actions only take effect in ODC builds, not in
standalone Capacitor apps.
- Do not repeat the ODC setup steps in the terminal.