# Building a canvas app from source, via `pac` CLI

Everything here was established by testing, because most of it is not documented. Following it will save you a day.

**When to use this:** the customer needs to click the app themselves, or it must live in a tenant. For a fast visual concept, use the HTML pattern instead (`04-html-concept-demo.md`).

---

## 0. Prerequisites

```powershell
pac auth list                 # confirm you're authenticated to the right tenant
pac env list                  # find the environment ID
pac env select --environment <envId>
pac canvas list               # confirm you can see apps
```

`pac` and your browser may be signed into **different tenants**. Demo/CDX environments frequently live outside your corporate tenant — so the maker portal in your browser will 404 on an environment that `pac` can reach perfectly. Trust `pac`.

---

## 1. THE critical finding: the modern YAML layout is read-only

`pac canvas unpack` supports two layouts:

| Layout | Editable? |
|---|---|
| `SourceCode` (modern, `.pa.yaml`) | ❌ **No** |
| `Experimental` (classic, `.fx.yaml`) | ✅ **Yes** |

The modern layout writes a binary `.msapr` blob alongside the YAML, and **`pack` reads the blob, not your YAML.**

Proof: replace a screen's `.pa.yaml` with deliberately invalid YAML and pack it —

```
Packing succeeded.
```

Your edits are silently discarded. Delete the `.msapr` and pack fails outright with *"Unable to determine the layout."*

**Therefore: author with `--layout Experimental`.** It is deprecated and warns loudly, but it is the one that honours edits.

```powershell
pac canvas unpack --msapp app.msapp --sources .\src --layout Experimental
# edit .\src\Src\*.fx.yaml
pac canvas pack   --sources .\src --msapp out.msapp --layout Experimental --overwrite
```

A `Warning PA2001: Checksum mismatch` after editing is expected and harmless.

---

## 2. Start from a real app, not from scratch

Create a blank canvas app in the maker portal, download it, and use it as your skeleton:

```powershell
pac canvas download --name "My Shell" --file-name base.msapp
pac canvas unpack --msapp base.msapp --sources .\src --layout Experimental
```

You inherit a valid `CanvasManifest.json`, `Themes.json`, `Entropy`, and checksums — all of which are tedious to synthesise.

**Also download an app that already uses the controls you need** (galleries, labels, buttons, HTML viewer). You'll harvest template registrations and package files from it.

Older apps may refuse to unpack in the modern layout (`MSAppStructureVersion 2.0 is below the minimum supported version 2.4.0`) — unpack them with `--layout Experimental` instead.

---

## 3. Control types: harvest, don't guess

Real template identifiers, from a working app:

```
label · button · rectangle · image · htmlViewer · icon.Cancel
gallery.galleryVertical · groupContainer.manualLayoutContainer
dropdown · pieChart · group · screen · appinfo
```

Harvest them:

```powershell
Select-String -Path .\src\Src\*.fx.yaml -Pattern '^\s*\w[\w\d_]*\s+As\s+([\w\.'']+)'
```

Once the MCP is available, `list_controls` returns the full catalogue (127 entries, including the modern set).

`pac canvas validate` is **retired** — *"'pac canvas validate' is no longer supported."*

---

## 4. Three things that must be registered or pack crashes

### a. `ControlTemplates.json`

Every control type you use needs an entry. A blank app registers only four (`appinfo`, `groupContainer`, `Host`, `screen`). Using a gallery without registering it produces:

```
Error PA3001: Internal error. Object reference not set to an instance of an object.
   at ...GalleryTemplateTransform.BeforeWrite(BlockNode control)
```

Merge the missing entries from the app you harvested:

```powershell
$a = Get-Content .\src\ControlTemplates.json -Raw | ConvertFrom-Json
$b = Get-Content .\probe\ControlTemplates.json -Raw | ConvertFrom-Json
foreach ($n in @('label','button','rectangle','htmlViewer','image','icon','gallery','galleryTemplate','group')) {
  if (-not $a.PSObject.Properties.Name.Contains($n)) {
    $a | Add-Member -NotePropertyName $n -NotePropertyValue $b.$n -Force
  }
}
$a | ConvertTo-Json -Depth 10 | Set-Content .\src\ControlTemplates.json -Encoding UTF8
```

A gallery needs **both** `gallery` and `galleryTemplate`.

### b. `pkgs\*.xml`

Widget templates also need their package file, or you get a wall of:

```
Warning PA2002: Widget control template: , version 2.5.1 was not found in the pkgs directory
```

Copy them across (`label_2.5.1.xml`, `button_2.2.0.xml`, `gallery_2.13.2.xml`, `rectangle_2.3.0.xml`, `htmlViewer_2.1.0.xml`, `icon_2.5.0.xml`, `image_2.2.2.xml`).

### c. Screen registration

New screens must be listed in **both**:
- `CanvasManifest.json` → `ScreenOrder`
- `Other\Src\_EditorState.pa.yaml` → `EditorState.ScreensOrder`

---

## 5. Control names are globally unique across the app

Not per screen. A shared header repeated on five screens collides:

```
Error PA3008: Symbol 'recTop' is already defined. Previously at Screen1.fx.yaml(4,5,4,25)
```

Suffix per screen (`recTopA`, `recTopB`, …). If you generate screens from a script, pass a screen suffix into the shared-chrome function.

---

## 6. YAML gotchas

**Blank lines inside a block scalar break the parser:**
```
Error PA3003: Parse error: Property should be at same indent level
```
Keep multi-line formulas as one continuous block.

**Multi-line formula syntax:**
```yaml
    Text: |-
        ="line one
        line two"
```

**PowerShell here-strings eat `$`.** `"$1.92M"` becomes `.92M` in a double-quoted here-string. Use a single-quoted here-string, or escape with a backtick. **Grep your generated output for the currency symbol before packing.**

---

## 7. Data without connectors

For a concept demo, avoid connectors entirely — put static collections in `App.OnStart`:

```yaml
App As appinfo:
    OnStart: |-
        =ClearCollect(colAccounts,
            {Name:"...", Value:"...", Colour:RGBA(196,49,75,1)},
            ...
        )
```

Benefits: no connection prompts, no data-source permissions, and the app can be shared with anyone — including the customer — without granting access to your demo org.

---

## 8. Packaging into a solution (for sharing/import)

`pac canvas pack` gives you an `.msapp`. To hand it over or move it between environments, wrap it in a solution.

Fastest reliable route: **build a template from a real export.**

```powershell
pac solution init --publisher-name MyPub --publisher-prefix myp --outputDirectory .\sol
# edit sol\src\Other\Solution.xml -> UniqueName
pac solution pack --zipfile sol-empty.zip --folder .\sol\src
pac solution import --path sol-empty.zip --publish-changes
pac solution add-solution-component --solutionUniqueName <name> --component <canvasAppId> --componentType 300
pac solution export --path tmpl.zip --name <name> --overwrite
pac solution unpack --zipfile tmpl.zip --folder .\tmpl
```

Now `.\tmpl` has the exact structure. Swap in your own `.msapp`, write a matching `<AppName>.meta.xml`, and point `Solution.xml` → `RootComponents` at your app schema name.

**Required, or packing fails:** each canvas app needs a `BackgroundImageUri` composite file alongside the `.msapp`:

```
Error: Missing or more than 1 composite reference 'BackgroundImageUri' found for canvas app ...
```

Copy the one from the template export and reference it in the meta XML. Also strip `<MissingDependencies>` if the template carried dependencies you don't have.

---

## 9. `pac canvas pack` silently no-ops if the target exists

It prints **help text** rather than an obvious error, and the previous file remains:

```
Error: The value passed to '--msapp' is invalid. The output file '...' already exists.
```

...which is easy to miss inside a wall of usage output. **Always pass `--overwrite` and check the timestamp:**

```powershell
(Get-Item out.msapp).LastWriteTime
```

Otherwise you will confidently ship a "fixed" solution containing the old broken app.

---

## 10. Verify by round-trip, then from the service

```powershell
pac canvas pack   --sources .\src --msapp out.msapp --layout Experimental --overwrite
pac canvas unpack --msapp out.msapp --sources .\verify --layout Experimental
Select-String -Path .\verify\Src\*.fx.yaml -Pattern ' As '        # controls survived?
Select-String -Path .\verify\Src\App.fx.yaml -Pattern 'ClearCollect'
```

After importing, **download it back from the service** and confirm the platform stored what you sent. That is the only real proof.

Find an app's ID in a new environment:

```powershell
pac org fetch --xml '<fetch><entity name="canvasapp"><attribute name="canvasappid"/><attribute name="name"/><filter><condition attribute="name" operator="eq" value="myp_myapp_xxxxx"/></filter></entity></fetch>'
```

---

## 11. The Canvas Authoring MCP (optional, but worth it)

If you have the Canvas Authoring MCP server, it adds live validation and app sync.

**Registration:** the config file is host-specific. For Microsoft Scout it is `~/.scout/m-mcp-servers.json` — **not** `~/.copilot/m-mcp-servers.json`, which belongs to GitHub Copilot CLI. Editing the wrong one produces a server that never spawns and no error message.

A server loads only if it is **both** registered in that file **and** enabled under `permissions.servers` in host settings.

Invoke the executable directly rather than a batch shim:
```json
{ "command": "C:\\Program Files\\dotnet\\dotnet.exe",
  "args": ["dnx","Microsoft.PowerApps.CanvasAuthoring.McpServer","--yes","--prerelease"] }
```

Verify a server by hand before blaming the host — pipe `initialize` + `tools/list` into it over stdio and check it answers.

**Coauthoring must be enabled per app, in Studio, by a human:**
Settings → Updates → **New** → search "coauthor" → toggle on, then reload. It is GA, needs no tenant setting or licence, and **cannot** be set via `pac`, a Dataverse column, or the `.msapp` (`AppPreviewFlagsMap` has no such key). The MCP is a *coauthor participant*, not a standalone editor — it joins a live session.

**Environment gateway can be broken per-cluster.** If `connect` fails with an SSL/TLS reset, test the gateway directly:

```powershell
curl.exe -sS -o NUL -w "%{http_code}" "https://<envid-30chars>.<last2>.environment.api.powerplatform.com/gateway/cluster"
```

A healthy environment returns JSON with a cluster name. If one environment resets while another in the same tenant returns 200 using the identical hostname derivation, the fault is that cluster — not your config. Move the app to a working environment (trivial when it has no data sources).

### The payoff

`compile_canvas` performs real Power Fx validation and catches errors that `pac canvas pack` **and** solution import both accept silently. Example: `SortByColumns(col, "Field", Descending)` packs and imports without complaint but is invalid — it must be `SortOrder.Descending`, and the gallery would have failed live.

**Nothing else in the toolchain catches this.** If you have the MCP, run `compile_canvas` before every handover.
