/generate-native-extension
SkillDev toolsRead the approved PRD.md and generate the native sources for a third-party PAM control (the compiled `.ppmplugin` track) — iOS Obj-C `<Pascal>Module` plus optional system-frameworks podspec, Android Kotlin `<Pascal>Module` with build.gradle, AndroidManifest and ReactPackage, a dev-only private package.json (react + react-native devDeps for the builds), and the committed `./manifest.json` dispatch contract the PCF and build stage both read. No TypeScript INativeExtension layer — the contract is the manifest plus the native modules' dispatch surface. Emits the layout in shared/repo-layout.md and generates substantially complete native code (compiled later by /build-android-binary and /build-ios-binary, not here). Local only — writes files, runs no git and touches no remote or feed. PCF is generated by /generate-pcf-companion; the bundle is built by /generate-ppmplugin.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the /generate-native-extension skill
What this skill tells your AI
The instructions your AI receives, as published by microsoft/power-platform-skills in plugins/power-apps-mobile-extension/skills/generate-native-extension/SKILL.md and read by ahel’s review.
Reads PRD.md in the working directory and writes the native sources for a third-party PAM control following the layout in shared/repo-layout.md. This is the native-only (compiled .ppmplugin) track — there is NO TypeScript INativeExtension / handleMessageAsync layer; the wrap host dispatches straight to NativeModules.<Pascal>Module.<method> per the manifest's receivers contract (see shared/ppmplugin-format.md §2). The output is substantially complete native code so the engineer starts at customizing OS-specific code, not writing boilerplate.
This skill writes the native module half of the repo (ios/, android/, optional podspec, dev-only package.json) and the committed ./manifest.json — the dispatch-contract source of truth. The manifest is authored here, alongside the native code it describes, because every field in it is derived from the names this scaffold emits (getName(), the @ReactMethod list, the package class); authoring it now means the Companion PCF (/generate-pcf-companion) reads a real contract instead of re-deriving one, so the composite key <name>/<receiver> can't drift between the PCF and the module. The build stage /generate-ppmplugin-manifest (inside /generate-ppmplugin) then validates + reconciles + stages this manifest rather than authoring it from scratch. The Companion PCF is generated separately by /generate-pcf-companion because it requires pac CLI and a different toolchain.
Step 1 — Read the shared docs and the PRD
Before any write:
-
Apply the per-skill minimal prereq policy (
shared-instructions.md §1.5). This track is self-contained (shared-instructions §0a) and uses only the working tree and public package registries. This skill needs no toolchain to write the files — optionally Node + pnpm to seed the dev-onlypackage.json's devDeps from the public npm registry (used later by/build-android-binary//build-ios-binary, not here). Step 4's smoke check is a structural self-check — it does NOT compile anything. Run the/generate-native-extensioncheck fromprereq-check.md(git required; Node/pnpm optional — there is no "baseline" check in this self-contained track).Print the prereq status as a visible block per
shared-instructions.md §9.2before continuing:━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Prereq check — /generate-native-extension ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🟢 ✓ git installed 🟢 ✓ Node 20+ installed (optional — only to seed package.json devDeps from public npm) 🟢 ✓ pnpm installed (optional — same) 🟢 checks passed. Ready to proceed.If
gitis missing, print its→ Fix:line and STOP. Node/pnpm are optional here — if absent, note them asn/a (devDeps seed deferred to build skills)rather than failing. -
Read
shared/naming-conventions.md— the derived-identifier table is canonical, including theModule-suffix rule for the native module symbol. Derive all file paths and class names from §2 of the PRD using that table; do not invent. -
Read
shared/ppmplugin-format.md— §2 (the runtime dispatch contract:<name>/<receiver>→NativeModules.<nativeModule>.<method>, where<nativeModule>=<Pascal>Module) and §4 (the upload-compatibility checks that the native module symbol must satisfy). The native modules this skill emits dispatch straight off that contract — there is NO TSINativeExtensionlayer mediating; see §3.3 below. -
Read
shared/repo-layout.md— the exact tree, file list, andpackage.jsonshape to emit. -
Read
./PRD.mdfrom the current working directory. If missing or empty, STOP withBLOCKED: PRD.md not found — run /design-native-extension-feature first. -
Read
./.extension-state.mdif present. If the phase showsscaffold-complete, ask the user whether to regenerate (with confirm — overwrites files), resume (only fill in missing files), or abort.
The structural patterns this skill needs to emit (iOS module shape, Android module shape, podspec, package.json) are fully prescribed in this SKILL.md (§3.1–§3.7) and in shared/repo-layout.md. Do NOT fetch the reference extension repo at runtime — its lessons are already encoded here, and fetching it would risk copying PDF-specific code into a non-PDF extension.
If any read fails, STOP and report which file is missing.
Step 2 — Confirm the scaffold plan with the user
Print a concise summary derived from the PRD, then gate on approval before any write.
Scaffold plan
─────────────
Repo: powerapps-<kebab>
package: <kebab>-control (dev-only, private — not published)
Class: <Pascal>
Native module: <Pascal>Module → NativeModules.<Pascal>Module (== ./manifest.json receivers[].nativeModule)
iOS class: RCT<Pascal>Module (+moduleName returns <Pascal>Module)
Android module: <Pascal>Module (com.powerapps.<lower>)
Podspec: <Pascal>Extension.podspec (optional, system-frameworks-only)
Dispatch contract: ./manifest.json (committed — written by this skill; read by the PCF + build stage)
Frameworks
iOS: <list from ARCHITECTURE §1.2>
Android: <list from ARCHITECTURE §1.3>
Operations (<count from PRD §4>): <comma-separated names>
Pattern: <one-shot | streaming | two-way>
Error codes: <count from ARCHITECTURE §5>
Target directory: <cwd> (writes <N> files; no existing files will be overwritten without confirm)
Distribution: the compiled `.ppmplugin` bundle (built later by /generate-ppmplugin). This skill is purely local — no remote, no feed, no registry.
Use AskUserQuestion (single-select):
Proceed with this scaffold?
- Yes — generate the files (recommended): write the control's sources into the current directory. This skill does not run git — no
git init, no staging, no commit (the control lives in your existing repo; you commit when you're ready).- Edit the PRD first — exit; user re-runs
/design-native-extension-featureto adjust.- Cancel
Step 3 — Generate the files
Write files in the order below. After each top-level group, print a one-line progress update (✓ wrote ios/ (3 files)). Don't dump file contents — the user sees the diff via the IDE.
Every file path is relative to the current working directory (the repo root). Names are derived per shared/naming-conventions.md.
3.1 Top-level repo files
Write:
-
.gitignore— emit exactly the following entries:- Node:
node_modules/,dist/,build/ .ppmpluginbuild staging — MANDATORY:ppmplugin/(the gitignored staging dir where/generate-ppmpluginwrites the staged copy of the manifest, the binaries, and the final bundle — never committed. NOTE: the committed source-of-truthmanifest.jsonlives at the repo root (./manifest.json, written below), NOT underppmplugin/— do not gitignore it; seeshared/ppmplugin-format.md §1)- OS / editor:
.DS_Store,.idea/,.vscode/ - Claude Code local state (per-user, not shared):
.claude/ - Env:
.env*(but allow!.env.example) - iOS build:
Pods/,*.xcworkspace,DerivedData/,*.xcodeproj/xcuserdata/ - Android build:
*.iml,.gradle/,local.properties,captures/,.externalNativeBuild/,.cxx/ - PCF build dirs only — NOT the
pcf/folder itself; source files (index.ts,ControlManifest.Input.xml,package.json,pcfconfig.json, etc.) stay tracked:pcf/**/{out,Solutions,node_modules,obj,bin,generated}/ - Test-harness artifacts:
test-harness/*.msapp - Skill-generated backups:
*.bak.*(skills that replace tracked content may save a timestamped backup; those are intentionally local-only) - Design-time previews:
.pcf-preview/(HTML mockup of the PCF as it appears in Canvas Studio — written by/design-native-extension-featureStep 8.0 for visual review; regenerated each design iteration; not a source-of-truth artifact)
- Node:
-
package.json— per the dev-only shape inshared/repo-layout.md§"package.jsonshape (dev-only)". Fill inname(a plain local name, e.g.<kebab>-control) anddescriptionfrom the PRD.versionstarts at0.1.0. Set"private": true.This manifest is never published — no
publishConfig, no feed registry, nofilesarray, nomain/types, no.npmrc. Its only job is to pin the React Native version the native builds compile against:{ "name": "<kebab>-control", "version": "0.1.0", "private": true, "description": "<from PRD §1>", "devDependencies": { "react": "18.2.0", "react-native": "0.79.7" } }The
react-nativedevDep supplies the iOS headers (/build-ios-binary) and pins thereact-androidcoordinate the Android build resolves (/build-android-binary); add any other build-time devDeps the native modules need. All deps resolve from the public npm registry — there is no internal feed. -
manifest.json(repo root, committed — the dispatch-contract source of truth) — author it now from the names this scaffold emits, pershared/ppmplugin-format.md§2 (schema) + §3 (derivation). This is the single artifact the Companion PCF (/generate-pcf-companion) and the build stage (/generate-ppmplugin-manifest) both read; authoring it here, next to the native code it describes, is what keeps the composite key<name>/<receiver>from drifting between the PCF and the module. Fields:name=kebab(<Pascal>)of the class name (not the repo/capability name) — e.g. classPenInput→pen-input.version= thepackage.jsonversion (0.1.0).abi={ "compatibleShells": ">=1.0.0", "builtAgainst": "1.0.0" }(default; the build skills don't change it).receivers[]= a single entry{ "name": "<Pascal>Extension", "nativeModule": "<Pascal>Module", "methods": [<every @ReactMethod / RCT_EXPORT_METHOD name emitted in §3.4 / §3.5>] }.nativeModuleMUST equal AndroidgetName()and the iOS+moduleNamereturn value — theModule-suffixed name (the reserved-name dodge).entrypoints= declare every platform this scaffold generated (so the committed manifest is the full contract; the build stage trims it to the shipped target):- Android →
"android": { "dex": "<Pascal>Plugin.dex", "packageClass": "com.powerapps.<lower>.<Pascal>Package" } - iOS →
"ios": { "framework": "<Pascal>Plugin", "moduleClass": "RCT<Pascal>Module" }
- Android →
This is a logical contract, not a built artifact — it lists the platforms the module supports; the per-platform binaries are compiled later and the staged copy under
ppmplugin/staging/is reconciled down to whatever actually ships. Do NOT emit anyentrypoints.js/extension.hbc/extensionClassName/jsLayerfield — those are SDK-era leakage/audit-ppmpluginrejects. (The build stage re-runs the full validator on this file, so a malformed manifest is caught either way — but emit it correctly here.) -
README.md— one-page user-facing doc tailored to the control. Sections: "What's in the box" (the compiled.ppmpluginbundle + PCF companion), "Build" (run/generate-ppmpluginto produce the.ppmplugin), "Architecture" (a Mermaid-or-ASCII diagram of Canvas formula → PCF → wrap-bridge →NativeModules.<Pascal>Module), "Development" (pnpm installto seed devDeps; native code is compiled by the build skills, not here), "Reference docs" (link toshared/ppmplugin-format.md). Use the PRD's §1 Summary verbatim. Drive every section from the PRD — never inject example values, prose, or screenshots from any other control's README. -
CHANGELOG.md— single entry:# Changelog ## 0.1.0 — <ISO date> - Initial scaffold for <Human-Readable Name> native control. - Generated by pam-native-extensions plugin from PRD.md. -
LICENSE— MIT.
3.2 The podspec (optional, at repo root)
Write <Pascal>Extension.podspec at the repo root (NOT inside ios/) only if ARCHITECTURE §1.2 names additional iOS system frameworks the module links. The .ppmplugin iOS build (/build-ios-binary) compiles from a throwaway staged Xcode project and does NOT npm-autolink against this podspec — so it lists system frameworks only (no React-Core / RN-CLI autolink dependency, no remote source). It exists for local pod lib lint convenience, not the bundle build. Template:
require "json"
package_json = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
s.name = "<Pascal>Extension"
s.version = package_json["version"]
s.summary = "<one-line description from PRD>"
s.description = <<-DESC
<2-3 sentence description from PRD — what it does, what it bridges to>
DESC
s.license = "MIT"
s.author = { "Author" => "" }
s.platform = :ios, "<min-deployment-target from ARCHITECTURE §1.2>"
s.source = { :path => "." }
s.source_files = "ios/**/*.{h,m}" # change to {h,m,swift} if Swift used
s.frameworks = <comma-quoted list of SYSTEM frameworks from ARCHITECTURE §1.2>
# No React-Core dependency: the .ppmplugin build resolves RN headers from the
# react-native devDep in package.json, not via CocoaPods autolinking.
end
3.3 No TypeScript layer — the dispatch contract
This is the native-only track: there is no src/ TypeScript layer, no src/<Pascal>Extension.ts, no src/types.ts, no INativeExtension / handleMessageAsync implementation, and no sendAsync transport. (Those belong to the first-party SDK track — NOT in this track.) Do NOT generate any of them; reintroducing a TS contract layer here produces SDK-era leakage that /audit-ppmplugin rejects.
The contract instead is the manifest's runtime dispatch (shared/ppmplugin-format.md §2): the wrap host routes a call by the composite key <name>/<receiver> straight to NativeModules.<Pascal>Module.<method>(args, promise). There is no JS mediator. This means:
- The request shape (the
argsobject) and response shape (the object the promise resolves with) from ARCHITECTURE §4 are realized directly in the native@ReactMethod/RCT_EXPORT_METHODsignatures + their JSON responses — see §3.4 (iOS) and §3.5 (Android). The per-operation JSON parsing, request validation, operation branching, and error-code responses that a first-party TShandleMessageAsyncwould have done are emitted inside each native method instead. That dispatch logic is the valuable part this skill generates. - The
manifest.jsonthat declaresname,receivers[].method, andreceivers[].nativeModule(=<Pascal>Module) is written by this skill at the repo root (§3.1) — the native module symbols it emits and the manifest'sreceivers[]are authored together, so they can't disagree./generate-ppmplugin-manifestlater validates + reconciles + stages this file rather than re-authoring it (§2/§3 below +shared/ppmplugin-format.md §3). - The error-code set from ARCHITECTURE §5 is realized as the string codes the native
errorJson(code, message)helpers emit (§3.4 / §3.5), each paired with a human-readablemessage— there is no TS error-union type to declare. These codes are the stable strings from the canonical catalogshared/error-codes.md(Canvas formulas branch on them, so they must not drift); emit exactly the catalog spelling for any code ARCHITECTURE §5 reuses. The PCF reads both: theerrorcode to branch on, themessageto surface as itsErrorMessageoutput.
3.4 iOS (ios/)
Write:
-
ios/RCT<Pascal>Module.h— minimal Obj-C header importing<React/RCTBridgeModule.h>, declaring@interface RCT<Pascal>Module : NSObject <RCTBridgeModule> @end. -
ios/RCT<Pascal>Module.m— the implementation. Generate complete working code, not TODO placeholders. For each operation in PRD §4, the per-operation §3. block prescribes every implementation decision (framework, hosting, key APIs, export shape, edge case handling). Generate the implementation verbatim from §3.:- Imports: include
RCT<Pascal>Module.h,UIKit, plus every framework named in ARCHITECTURE §3.'s "Framework / class" field for any operation (e.g.#import <PencilKit/PencilKit.h>if any §3. names PencilKit). - Module identity: do NOT emit
RCT_EXPORT_MODULE(...)in a wrap plugin framework. That macro registers via+loadand_RCTRegisterModule, which is not visible to the framework'sdlopenflat namespace. Instead emit a class method+ (NSString *)moduleName { return @"<Pascal>Module"; }— theModule-suffixed name. The Obj-C class name staysRCT<Pascal>Module(matchingentrypoints.ios.moduleClass), while+moduleNameMUST equal the manifest'sreceivers[].nativeModuleand JS seesNativeModules.<Pascal>Module. Do NOT strip the suffix. + (BOOL)requiresMainQueueSetupreturningNOunless any §3. requires main-thread init.initsafety — the module is instantiated eagerly at load via[cls new], soinitMUST NOT throw or do heavy/side-effecting work (ppmplugin-format §5). Do not acquire hardware, registerNSNotification/KVO observers, or touchAVCaptureSession/CLLocationManagerininit— defer to the firstRCT_EXPORT_METHODcall (lazy), and wrap any unavoidable init work in@try/@catch. An uncaught exception ininitcrashes the host at launch (the iOS analogue of the Android Looper-less-Handlercrash).- For each operation, write an
RCT_EXPORT_METHODtaking exactly oneNSDictionary *requestparameter, thenRCTPromiseResolveBlock resolve,RCTPromiseRejectBlock reject— e.g.RCT_EXPORT_METHOD(capturePenInput:(NSDictionary *)request resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject). This matches the wrap dispatch contract: the PCF sendsargs: [request](a one-element array) spread positionally, so the method's first positional param is the request dictionary (ppmplugin-format §2). Read fields offrequest(request[@"…"]); do NOT expand into multiple positional params. Also: the Obj-C class MUST instantiate via a no-arg[cls new]after the runtime loads it — don't add a custom designated initializer that takes arguments. The body implements §3.'s iOS spec completely:- The hosting setup ("dedicated UIViewController presented modally, full-screen" → emit a
UIViewControllersubclass or inline VC +presentViewController:animated:completion:). The presented VC'sviewDidLoadMUST constrain custom content views toview.safeAreaLayoutGuide, notviewdirectly — this prevents content from intruding under the notch / Dynamic Island / home indicator. SetmodalPresentationStyle = UIModalPresentationFullScreen(or.pageSheetper ARCHITECTURE §3.). Add aUINavigationBarwith Done / CancelUIBarButtonItems for clear action affordance — same Material-toolbar-equivalent pattern as Android. - The key API calls in the order §3. specifies (e.g.
PKCanvasViewinit,PKToolPickerattachment, drawing capture) - Each Done/Cancel/dismiss handler as §3. specifies
- The export step as §3.'s "Export" line specifies (e.g.
drawing.image(from: canvas.bounds, scale: 2.0)→ PNG → base64) - Each edge case from §3.'s "Edge cases handled" list, with the exact behavior named (e.g. "User taps Cancel → resolve with USER_CANCELLED")
- The hosting setup ("dedicated UIViewController presented modally, full-screen" → emit a
- Threading: background work on
dispatch_get_global_queue; UI presentation ondispatch_get_main_queue. Long-running native work must not block the JS thread. - Error helper: emit
- (NSString *)errorJsonWithCode:(NSString *)code message:(NSString *)messagethat builds the dict@{@"status": @"error", @"error": code, @"message": (message ?: @"")}and serializes it viaNSJSONSerialization— the SAME serializer as the success helper. Do NOT usestringWithFormat: amessage(or code) containing a",\, or newline would emit invalid JSON, which the PCF's response parse would surface as a misleadingPARSEinstead of the real failure — defeating the whole point of the message. Themessageis a human-readable diagnostic that makes the failure debuggable from the PCF without a native debugger: for a caught exception passerror.localizedDescription; for a validation failure a specific reason (e.g.@"missing required field 'uri'"); forUSER_CANCELLEDa short note. Every error path calls this with BOTH a code and a message — never a bare code. - Success helper: emit
- (NSString *)successJsonWith:(NSDictionary *)resultthat builds{"status":"ok","result":<result>}viaNSJSONSerialization. - Error propagation — wrap the operation body so every failure reaches the PCF with a code AND a message. Any framework/runtime failure must
resolvewitherrorJsonWithCode:message:carrying a specific code and reason — never throw an uncaught Obj-C exception, crash, orresolveempty. Use@try/@catcharound risky synchronous work and resolve the@catchwithINTERNAL_ERRORplusexception.reason. - UI hygiene boilerplate for each presented
UIViewController'sviewDidLoad(mirrors Android's insets handling — prevents the most common iOS issue: content under safe areas, status bar, home indicator):- (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor systemBackgroundColor]; // Navigation bar with Done / Cancel — equivalent to Android's MaterialToolbar. UINavigationBar *navBar = [[UINavigationBar alloc] init]; navBar.translatesAutoresizingMaskIntoConstraints = NO; UINavigationItem *navItem = [[UINavigationItem alloc] initWithTitle:@"<Human-readable from PRD §2>"]; navItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(handleCancel)]; navItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(handleDone)]; navBar.items = @[navItem]; [self.view addSubview:navBar]; // Content view — the operation-specific surface (e.g. PKCanvasView, AVCaptureVideoPreviewLayer host). // Constrain to safeAreaLayoutGuide so content doesn't extend under the notch / home indicator. UIView *contentView = [[UIView alloc] init]; // Replace with operation-specific view per ARCHITECTURE §3.<n> contentView.translatesAutoresizingMaskIntoConstraints = NO; [self.view addSubview:contentView];
- Imports: include
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 859
- Forks
- 176
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
generate-native-extension- Source
- github.com/microsoft/power-platform-skills