# Add Server Logic

> Creates, edits, and manages Power Pages Server Logic files — server-side JavaScript that runs securely on the Power Pages runtime. Orchestrates the full lifecycle: gathering requirements, fetching documentation, implementing code, configuring site settings, and deploying. Use when the user wants to add server-side code, create API endpoints, or move logic from the browser to the server in their Power Pages site.

- Skill: `majiayu000/add-server-logic` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/add-server-logic`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/add-server-logic/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/add-server-logic

---


> **Plugin check**: Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding.

# Add Server Logic

Create and manage one or more Power Pages Server Logic files — server-side JavaScript that runs securely on the Power Pages runtime, hidden from the browser and protected by web roles and table permissions. Server Logic enables secure external API integrations, Dataverse operations, and custom business logic without exposing sensitive code or credentials to the client.

## Core Principles

- **Microsoft Learn is the source of truth**: Always fetch the latest documentation before writing code. The Server Logic feature is in preview and the SDK may change — never rely on cached knowledge alone.
- **No browser APIs, no dependencies**: Server Logic runs in a sandboxed server environment with ECMAScript 2023 support. There is no `fetch`, `XMLHttpRequest`, `setTimeout`, or any DOM API. No npm packages are available.
- **Five functions only**: A server logic file can only export these top-level functions: `get`, `post`, `put`, `patch`, `del`. The name `delete` is a reserved word in JavaScript and cannot be used.
- **Always return a string**: Every function must return a string. Use `JSON.stringify()` when returning objects or arrays.
- **Use TaskCreate/TaskUpdate**: Track all progress throughout all phases — create the todo list upfront with all phases before starting any work.

> **Prerequisites:**
> - An existing Power Pages code site created
> - The site **must** be deployed at least once (`.powerpages-site` folder must exist) — server logic files live inside `.powerpages-site/server-logic/`, so deployment is required before any server logic can be created

**Initial request:** $ARGUMENTS

---

## Workflow

1. **Verify Site Exists** — Locate the Power Pages project, explore existing patterns, and verify prerequisites
2. **Understand Requirements** — Determine the user intent and whether the solution needs one or more server logic files
3. **Fetch Latest Documentation** — Query Microsoft Learn for the most current Server Logic SDK reference
4. **Review Implementation Plan** — Present the plan to the user and confirm before writing code
5. **Implement Server Logic** — Create the approved `.js` and `.serverlogic.yml` files in `.powerpages-site/server-logic/<name>/`
6. **Configure Table Permissions** — *(Conditional: only if Server.Connector.Dataverse is used)* Set up table permissions for Dataverse tables accessed by the server logic
7. **Manage Secrets & Environment Variables** — *(Conditional: only if the server logic requires secrets)* Store sensitive values securely using Azure Key Vault (recommended) or direct environment variables in Dataverse
8. **Configure Site Settings** — Set up ServerLogic site settings if needed
9. **Client-Side Integration** — Help wire the server logic into the site's frontend code
10. **Verify & Test Guidance** — Validate the code and provide testing instructions
11. **Review & Deploy** — Present summary and offer deployment

---

## Phase 1: Verify Site Exists

**Goal**: Locate the Power Pages project root and confirm prerequisites

**Actions**:

1. Create todo list with all 11 phases (see [Progress Tracking](#progress-tracking) table)

### 1.1 Locate Project

Look for `powerpages.config.json` in the current directory or immediate subdirectories

**If not found**: Tell the user to create a site first with `/create-site`.

### 1.2 Read Existing Config

Read `powerpages.config.json` to get the site name and configuration:

### 1.3 Detect Framework

Read `package.json` to determine the frontend framework (React, Vue, Angular, or Astro). This is needed for Phase 8 (client-side integration guidance). See `${CLAUDE_PLUGIN_ROOT}/references/framework-conventions.md` for the full framework detection mapping.

### 1.4 Explore Existing Server Logic and Frontend Code

Use the **Explore agent** (via `Task` tool with `agent_type: "explore"`) to analyze the site for existing server logic patterns and frontend code that may call or need to call server logic endpoints.

**Prompt for the Explore agent:**

> "Analyze this Power Pages code site for server logic context. Check:
> 1. Does `.powerpages-site/server-logic/` exist? If yes, list all subdirectories and their .js files. Summarize what each server logic does (which functions it implements, what SDK features it uses). Also read the corresponding .serverlogic.yml files to check web role assignments.
> 2. Search the frontend source code (`src/**/*.{ts,tsx,js,jsx,vue,astro}`) for any existing calls to `/_api/serverlogics/` — these indicate server logic endpoints already being consumed.
> 3. Look for CSRF token handling patterns (`__RequestVerificationToken`, `_layout/tokenhtml`) — these show how the site currently makes authenticated API calls.
> 4. Check for any TODO/FIXME comments mentioning server logic, backend, or server-side processing.
> 5. Look for hardcoded API URLs, mock data, or placeholder fetch calls that might need to be replaced with server logic calls.
> 6. Check for any existing service layer or API utility files in `src/shared/`, `src/services/`, or similar directories that could be reused for server logic integration.
> 7. Read `.powerpages-site/web-roles/*.webrole.yml` files to list available web roles and their GUIDs — these are needed when creating the server logic metadata YAML.
> 8. For each existing server logic, assess whether it can be reused or safely extended for the requested capability instead of creating a brand-new server logic file. Call out any strong reuse candidates and explain why.
> Report all findings so we can avoid duplicating work and match existing patterns."

From the Explore agent's findings, note:
- **Existing server logic files** — what's already implemented, and which ones are candidates for reuse or extension
- **Frontend calling patterns** — how the site makes API calls (match this pattern in Phase 9)
- **Existing service/utility files** — reuse these when adding client-side integration
- **Gaps** — frontend code that references server logic endpoints that don't exist yet

### 1.5 Check Deployment Status (Mandatory)

Look for the `.powerpages-site` folder:


**If not found**: The site **must** be deployed before server logic can be created — server logic files live inside `.powerpages-site/server-logic/`. Tell the user:

> "The `.powerpages-site` folder was not found. Server logic files are stored inside this folder, so the site must be deployed at least once before creating server logic. Would you like to deploy now?"

Use `AskUserQuestion`:

| Question | Options |
|----------|---------|
| The `.powerpages-site` folder is required for server logic. Would you like to deploy the site now? | Yes, deploy now (Required), Cancel |

**If "Yes, deploy now"**: Invoke `/deploy-site` first, then continue to Phase 2.

**If "Cancel"**: Stop the workflow — server logic cannot be created without `.powerpages-site`.

**Output**: Confirmed project root, `.powerpages-site` exists, existing server logic (if any), available web roles

---

## Phase 2: Understand Requirements

**Goal**: Determine the user intent, identify whether one or more server logic files are needed, and capture the required HTTP methods for each item

**Actions**:

### 2.1 Analyze User Request

From the user's request, determine:

- **Intent shape**: Does the request map to a single server logic or multiple server logic?
- **Reuse opportunities**: Can an existing server logic satisfy or be safely extended for part of the request?
- **Server logic inventory**: For each required server logic, capture the purpose, suggested endpoint name, and whether it should be reused, extended, or created new
- **HTTP methods needed**: Which of the 5 functions should be implemented for each server logic (`get`, `post`, `put`, `patch`, `del`)

Prefer reuse or safe extension of an existing server logic when it already matches the domain, security model, and lifecycle of the requested capability. Only create a new server logic when reuse would make the existing file confusing, over-broad, or unsafe.

Prefer multiple server logic files when the use case naturally separates into different responsibilities, security boundaries, or lifecycle concerns. Examples:

- Separate read vs. write workflows with different web role requirements
- Distinct integrations with different external systems or site settings
- Independent business capabilities that would be harder to test or reason about if merged into one endpoint

### 2.1.1 Identify Validate-and-Execute Patterns

For each planned server logic item, determine whether it should **validate-and-execute** — meaning the server logic both validates a business rule AND performs the resulting Dataverse write, rather than just returning a validation result for the client to act on.

A server logic item should validate-and-execute when **any** of these are true:

| Condition | Example |
|-----------|---------|
| It enforces a state machine or lifecycle | Order status: Draft → Submitted → Approved |
| The write is conditional on a business rule that must be tamper-proof | "Only allow bid submission before the deadline" |
| The operation spans multiple tables atomically | Award a bid + reject all others + update event status |
| The write involves a computed or derived value | Server calculates a score and writes it |
| The client should not have direct write access to the field | Status fields with strict transition rules |

For each validate-and-execute item, note:
- **Which Dataverse writes the server logic will perform** (UpdateRecord, CreateRecord, etc.)
- **Which fields are being written** — these fields should NOT be writable via Web API from the client
- **What the server logic returns to the client** — typically a success/failure result with the before/after state, NOT a validation flag that the client acts on

**Anti-pattern to avoid**: A server logic item that only validates and returns `{ valid: true/false }`, expecting the client to make a separate Web API call to perform the write. This allows the client to skip validation and write directly.

### 2.1.2 Discover Dataverse Custom Actions

If any planned server logic item involves Dataverse operations, check whether the user's Dataverse environment has existing custom actions (Custom APIs or Custom Process Actions) that could be leveraged instead of building logic from scratch.

**Step 1 — Fetch custom actions:**

```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/list-custom-actions.js" "<ENV_URL>"
```

The script outputs a JSON object with:
- `customApis` — Modern Custom APIs with full request parameters and response properties
- `customProcessActions` — Legacy Custom Process Actions (activated only)
- `total` — Total count of both types combined

Each entry includes: `name`, `displayName`, `description`, `type` (`action` or `function`), `binding` (`unbound`, `entity`, or `entityCollection`), `boundEntity`, and `source` (`customApi` or `customProcessAction`). Custom APIs also include `requestParameters` and `responseProperties` arrays.

**Step 2 — Present and ask the user:**

If custom actions are found (`total > 0`), present a summary to the user grouped by binding type (unbound vs. entity-bound) and ask whether any should be used:

Use `AskUserQuestion`:

| Question | Options |
|----------|---------|
| Your Dataverse environment has `<total>` custom action(s). Would you like to use any of these in your server logic instead of writing the logic from scratch? | Yes, let me choose which ones to use; No, build everything from scratch |

Present the list clearly — for each action show: name, description, type (action/function), binding, and bound entity (if applicable). Group them as **Unbound** and **Entity-bound** for readability.

If the user says **No**, skip to Phase 2.2.

**Step 3 — Map custom actions to server logic items:**

If the user says **Yes**, for each server logic item being created, ask which custom action (if any) it should wrap:

Use `AskUserQuestion` for each server logic item:

| Question | Context |
|----------|---------|
| For the `<server-logic-name>` endpoint, which custom action should it use? | Present the list of custom actions with their names, descriptions, and binding types. Include **"None — build from scratch"** as an option. |

Record the mapping for each server logic item. For items that wrap a custom action, note:
- The custom action name (used in the `InvokeCustomApi` call)
- Whether it's a function (`GET`) or action (`POST`)
- The binding type and bound entity (if applicable)
- The request parameters and response properties (if available from Custom APIs)

This mapping will be used in Phase 5.3 when generating the server logic code, and will appear in the HTML plan (Phase 4) to indicate which items wrap existing custom actions.

### 2.2 Identify SDK Features Needed

Based on each planned server logic item's purpose, identify which Server SDK features are required:

| Feature | When to use |
|---------|-------------|
| `Server.Connector.HttpClient` | Calling external REST APIs (NOT Dataverse) |
| `Server.Connector.Dataverse` | Reading/writing Dataverse records (CRUD + `InvokeCustomApi` for Dataverse Custom APIs) |
| `Server.Context` | Accessing request parameters, headers, body |
| `Server.User` | User-scoped operations, role checks |
| `Server.Logger` | Always — every function should log entry/exit and errors |
| `Server.Sitesetting` | Reading site setting configuration values |
| `Server.EnvironmentVariable` | Reading Dataverse environment variable values directly via `Server.EnvironmentVariable.get(name)` — an alternative to `Server.Sitesetting` for non-secret config |
| `Server.Website` | Accessing site metadata |

### 2.3 Identify Secret Values

Determine whether any server logic item requires secret or sensitive configuration values that should not be hardcoded. Common examples:

| Scenario | Secret needed |
|----------|---------------|
| Calling an authenticated external API | API key, client secret, bearer token |
| Connecting to a third-party service | Connection string, access token |
| OAuth2 client credentials flow | Client ID + client secret |
| Webhook verification | Signing secret, shared key |

For each identified secret, capture:
- **Secret name**: A descriptive name (e.g., `ExchangeRateApiKey`, `PaymentGatewaySecret`)
- **Purpose**: Why the secret is needed
- **Site setting name**: The name the server logic will use with `Server.Sitesetting.Get()` (e.g., `ExternalApi/ExchangeRateApiKey`)
- **Environment variable schema name**: The Dataverse environment variable schema name (e.g., `cr5b4_ExchangeRateApiKey`)

These values will be used in Phase 7 to create the environment variables and site settings.

### 2.3.1 Key Vault Decision

If secrets were identified in Phase 2.3, ask the user now whether they want to use Azure Key Vault. This decision must happen before Phase 4 so the implementation plan can show the chosen secret management approach.

Use `AskUserQuestion`:

| Question | Options |
|----------|---------|
| This server logic requires secret values (e.g., API keys, client secrets). Azure Key Vault is the recommended way to store secrets securely. Would you like to use Azure Key Vault? | Yes, use Azure Key Vault (Recommended), No, store directly as environment variable |

Record the user's choice — it will be shown in the HTML plan (Phase 4) and executed in Phase 7.

### 2.4 Confirm with User

If the requirements are ambiguous, use `AskUserQuestion` to clarify:

| Question | Context |
|----------|---------|
| What should this server logic solution do overall? | If the purpose is unclear |
| Should this be one server logic or multiple server logic? | If the request could reasonably be modeled either way |
| Which HTTP methods does each server logic need? | If not specified — suggest based on the use case (e.g., read-only = GET, form processing = POST) |
| Does each server logic need to call external APIs, Dataverse, or both? | Determines which connectors to use |
| What should each server logic be named? | Suggest URL-friendly names based on the responsibilities |
| Does the server logic need any secret or sensitive values (API keys, client secrets, tokens)? | If the server logic calls authenticated external APIs or services |

**Output**: Clear understanding of the overall intent, the list of server logic items to reuse/extend/create, their HTTP methods, SDK features needed, and any secrets required

---

## Phase 3: Fetch Latest Documentation

**Goal**: Discover and read all current Server Logic documentation from Microsoft Learn before writing any code

This step is critical because Server Logic is a preview feature and the SDK surface may change. The documentation on Microsoft Learn is the authoritative source.

**Actions**:

### 3.1 Follow the Documentation Reference

Use the reference document below as the source of truth for how to discover, classify, fetch, and reconcile Server Logic documentation:

> Reference: `${CLAUDE_PLUGIN_ROOT}/skills/add-server-logic/references/server-logic-docs.md`

Follow that reference to:

- Search Microsoft Learn dynamically for all current Server Logic docs
- Fetch the core reference pages and any relevant scenario-specific pages
- Search for current code samples
- Reconcile the discovered documentation with the known SDK baseline in the reference

### 3.2 Extract Task-Specific Notes

From the fetched docs, extract and note the items that matter for the current task:

- All SDK method signatures, parameter types, and return types
- Current supported HTTP methods and function signatures
- Site settings and their defaults
- Security model (web roles, table permissions, CSRF)
- Client-side calling patterns and response format
- Any new methods or breaking changes discovered in Microsoft Learn

**Output**: Up-to-date SDK reference verified against all relevant Microsoft Learn documentation pages

---

## Phase 4: Review Implementation Plan

**Goal**: Present the implementation plan to the user and confirm before writing any code

**Actions**:

### 4.1 Prepare the Plan Data

Build the server logic plan data and render the HTML plan before asking for approval.

> Reference: `${CLAUDE_PLUGIN_ROOT}/skills/add-server-logic/references/server-logic-plan-data-format.md`

The rendered plan should summarize:

- The number of server logic items being created or reused
- Each endpoint name, API URL, and files to be created
- The functions that will be implemented and what each one does
- The SDK features, external services, and Dataverse tables involved for each item
- The web roles, security constraints, and site settings that apply to each item
- Any secrets or sensitive values that will be stored as environment variables (with or without Azure Key Vault). If the user chose Azure Key Vault in Phase 2.3.1, populate `SECRETS_DATA` with `useKeyVault: true` and the list of secrets — the HTML plan will render a Key Vault banner explaining the security benefits and show which secrets each server logic depends on. If no secrets are needed, set `SECRETS_DATA` to `null`.
- The expected next steps after approval

### 4.2 Render the HTML Plan

Generate the HTML plan file from the template and open it in the user's default browser before asking for approval.

When working inside a Power Pages project, write the plan to:

```text
<PROJECT_ROOT>/docs/serverlogic-plan.html
```

Create the `docs/` folder if it does not already exist. Keep this HTML file inside the repository so it can be reviewed and committed with the rest of the server logic work.

Do **not** hand-author the HTML. Use the render script:

```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/render-serverlogic-plan.js" --output "<OUTPUT_PATH>" --data "<DATA_JSON_PATH>"
```

The render script refuses to overwrite existing files. Before calling it, check if the default output path (`<PROJECT_ROOT>/docs/serverlogic-plan.html`) already exists. If it does, choose a new descriptive filename based on context — e.g., `serverlogic-plan-exchange-rate.html`, `serverlogic-plan-apr-2026.html`. Pass the chosen name via `--output`.

### 4.3 Present Plan Summary

Do **not** present a second detailed plan in the CLI. The HTML file is the single detailed plan artifact.

In the CLI, give only a brief summary that points the user to the HTML plan open in the browser. Keep it to:

- Total server logic count
- Whether the plan is creating, updating, or reusing items
- Whether web roles, table permissions, or site settings are involved
- The actual output path returned by the render script
- A note that the browser-opened HTML contains the full details

Do not restate the per-server-logic breakdown, rationale, role assignments, or function details inline in the CLI unless the user explicitly asks for a text version. Tell the user where the detailed HTML plan file was saved, that it has been opened in the browser for review, and that the repo copy of the plan will be committed with the implementation artifacts unless the user asks to discard it.

### 4.4 Confirm with User

Use `AskUserQuestion`:

| Question | Options |
|----------|---------|
| Here's the implementation plan for this server logic work. Does it look correct? | Approve and implement (Recommended), Request changes, Cancel |

**If "Request changes"**: Ask what needs to change, update the plan, and present again.

**If "Cancel"**: Stop the workflow.

**Output**: User-approved implementation plan

---

## Phase 5: Implement Server Logic

**Goal**: Create each approved server logic `.js` file and metadata YAML following the constraints verified in Phase 3

**Actions**:

### 5.1 Create Server Logic Folder

For each approved server logic item:

- **If the approved plan says `reuse`**: Do not create a new folder. Reuse the existing server logic as-is and only update the surrounding integration work if needed.
- **If the approved plan says `update` / extend**: Reuse the existing folder and update the existing `.js` / `.serverlogic.yml` files rather than creating duplicates.
- **If the approved plan says `create`**: Create the folder inside `.powerpages-site/server-logic/` (note: singular `server-logic`, no trailing 's'). Ensure the folder name matches that endpoint name exactly.

### 5.2 Read or Create Web Roles

Use the **Create Web Role** skill to determine which web roles are required for the approved server logic plan and to create any missing roles before writing metadata.

Do **not** assume every server logic should get every available role. Instead, determine the minimum set of roles required for each server logic based on its purpose, security model, and the approved plan.

Example web role file content:
```yaml
adx_anonymoususersrole: false
adx_authenticatedusersrole: true
description: Role for authenticated users
id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
name: Authenticated Users
```

In the skill workflow, explicitly invoke the **Create Web Role** skill when:

- The site has no suitable existing web roles
- The approved plan includes proposed roles that do not exist yet
- The role assignments need to be refined before metadata can be created

After the Create Web Role skill completes, read the resulting web role YAML files and collect the `id` and `name` values needed for each server logic's metadata YAML.

### 5.3 Create the Server Logic File

Repeat this step for each approved server logic item. Create or update `<PROJECT_ROOT>/.powerpages-site/server-logic/<name>/<name>.js` according to the approved plan status (`create`, `update`, or `reuse`) and follow these mandatory patterns:

#### Structure Rules

1. **Only top-level functions**: The file can only contain these 5 functions at the top level: `get`, `post`, `put`, `patch`, `del`. Only include the functions the user needs.
2. **Each function returns a string**: Use `JSON.stringify()` for objects/arrays.
3. **Each function has try/catch**: Every function must wrap its logic in a try/catch block.
4. **Each function logs**: Use `Server.Logger.Log()` at entry and `Server.Logger.Error()` in catch blocks.
5. **No imports or requires**: No `import`, `require`, or external dependencies.
6. **No browser APIs**: No `fetch`, `XMLHttpRequest`, `setTimeout`, `setInterval`, `console.log`, or DOM APIs.
7. **Async when needed**: Mark functions as `async` only when they use `await` (HttpClient calls). Dataverse connector methods (`Server.Connector.Dataverse.*`) are **synchronous** — do NOT use `async`/`await` with them.

#### Prohibited Script Patterns

The Power Pages server-side script validator rejects scripts containing certain patterns at runtime. Violations surface as `RTSL01: Script validation failed: prohibited pattern found - Pattern: <regex>` in diagnostics, and the function silently falls through without executing user code.

| Pattern | Regex | Caveat |
|---------|-------|--------|
| JavaScript `with` statement | `with\s*\(` | The regex matches the substring `with(` **anywhere** in the file — including inside string literals and inside other identifiers. OData filter functions like `startswith(`, `endswith(`, and `groupwith(` will trip it because they end with `with(`. |

**Workaround for OData functions** — split the literal so `with(` is not contiguous in source:

```javascript
// ❌ Triggers validator: "startswith(" contains the substring "with("
var query = "$filter=startswith(name,'INV-')";

// ✅ Split the literal — server still receives "startswith(name,...)"
var query = "$filter=startswith" + "(name,'INV-')";
```

The same trick applies to `endswith(`, `groupwith(`, and any other identifier that ends with `with(`.

#### Code Template

```javascript
// Server Logic: <name>
// Purpose: <description>
// API URL: https://<site-url>/_api/serverlogics/<name>

function get() {
    try {
        Server.Logger.Log("<name> GET called");

        // Access query parameters
        // const id = Server.Context.QueryParameters["id"];

        // Your logic here...

        return JSON.stringify({
            status: "success",
            method: "GET",
            data: null // replace with actual data
        });
    } catch (err) {
        Server.Logger.Error("<name> GET failed: " + err.message);
        return JSON.stringify({
            status: "error",
            method: "GET",
            message: err.message
        });
    }
}
```

#### Validate-and-Execute Template

When a server logic item is identified as validate-and-execute (see Phase 2.1.1), use this pattern. The key difference: the server logic reads the current state, validates the business rule, AND writes the result to Dataverse — all in one call. The client never writes the protected field directly.

```javascript
// Server Logic: <name>
// Purpose: Validate and execute <describe the operation>
// Pattern: Validate-and-execute — this endpoint both validates the business rule
//          and performs the Dataverse write. The client should NOT write <protected fields>
//          via Web API — all writes to those fields go through this endpoint.
// API URL: https://<site-url>/_api/serverlogics/<name>

function post() {
    try {
        Server.Logger.Log("<name> POST called");

        const body = JSON.parse(Server.Context.Body);
        const entityId = body.entityId;
        const targetStatus = body.targetStatus;

        // 1. Read the current record from Dataverse
        const current = Server.Connector.Dataverse.RetrieveRecord("<table-name>", entityId, "?$select=<status-field>");
        const currentStatus = current["<status-field>"];

        // 2. Validate the transition
        const allowedTransitions = {
            "Draft": ["Submitted"],
            "Submitted": ["Approved", "Rejected"],
            "Approved": ["Fulfilled"]
        };

        const allowed = allowedTransitions[currentStatus] || [];
        if (!allowed.includes(targetStatus)) {
            return JSON.stringify({
                status: "error",
                message: "Invalid transition: " + currentStatus + " → " + targetStatus + " is not allowed",
                currentStatus: currentStatus,
                targetStatus: targetStatus,
                allowedTargets: allowed
            });
        }

        // 3. Execute the write — server performs the Dataverse update
        const updateData = {};
        updateData["<status-field>"] = targetStatus;
        Server.Connector.Dataverse.UpdateRecord("<table-name>", entityId, JSON.stringify(updateData));

        Server.Logger.Log("<name> transition executed: " + currentStatus + " → " + targetStatus);

        // 4. Return the result — client receives confirmation, not a validation flag
        return JSON.stringify({
            status: "success",
            previousStatus: currentStatus,
            newStatus: targetStatus,
            entityId: entityId
        });
    } catch (err) {
        Server.Logger.Error("<name> POST failed: " + err.message);
        return JSON.stringify({
            status: "error",
            message: err.message
        });
    }
}
```

**Key differences from the basic template:**
1. The server logic reads the current state from Dataverse (not trusting the client's view)
2. It validates the business rule server-side
3. It writes the result to Dataverse via `Server.Connector.Dataverse.UpdateRecord`
4. It returns a success/failure result — NOT a `{ valid: true/false }` flag for the client to act on
5. The client calls this one endpoint — it does NOT make a separate Web API PATCH call

#### Custom Action Wrapping Template

When a server logic item wraps a Dataverse custom action (mapped in Phase 2.1.2), use this pattern with `Server.Connector.Dataverse.InvokeCustomApi`. The server logic acts as a portal-friendly wrapper, exposing the custom action through a `/_api/serverlogics/<name>` endpoint with proper web role authorization.

**Unbound action:**

```javascript
// Server Logic: <name>
// Purpose: Wraps Dataverse custom action "<custom-action-name>" for portal consumption
// Custom Action: <custom-action-name> (unbound, action)
// API URL: https://<site-url>/_api/serverlogics/<name>

function post() {
    try {
        Server.Logger.Log("<name> POST called — invoking custom action <custom-action-name>");

        const body = JSON.parse(Server.Context.Body);

        // Build the request payload matching the custom action's input parameters
        const payload = JSON.stringify({
            // "<ParameterName>": body.<clientFieldName>
        });

        const result = Server.Connector.Dataverse.InvokeCustomApi(
            "POST",
            "<custom-action-name>",
            payload
        );

        Server.Logger.Log("<name> custom action completed successfully");

        return JSON.stringify({
            status: "success",
            data: result
        });
    } catch (err) {
        Server.Logger.Error("<name> POST failed: " + err.message);
        return JSON.stringify({
            status: "error",
            message: err.message
        });
    }
}
```

**Entity-bound action:**

```javascript
function post() {
    try {
        Server.Logger.Log("<name> POST called — invoking bound action <custom-action-name>");

        const body = JSON.parse(Server.Context.Body);
        const entityId = body.entityId;

        const payload = JSON.stringify({
            // "<ParameterName>": body.<clientFieldName>
        });

        // Include the entity set and record ID, followed by the fully qualified action name
        const result = Server.Connector.Dataverse.InvokeCustomApi(
            "POST",
            "<entity-set-name>(" + entityId + ")/Microsoft.Dynamics.CRM.<custom-action-name>",
            payload
        );

        Server.Logger.Log("<name> bound action completed for entity " + entityId);

        return JSON.stringify({
            status: "success",
            data: result,
            entityId: entityId
        });
    } catch (err) {
        Server.Logger.Error("<name> POST failed: " + err.message);
        return JSON.stringify({
            status: "error",
            message: err.message
        });
    }
}
```

**Unbound function (read-only, GET):**

```javascript
function get() {
    try {
        Server.Logger.Log("<name> GET called — invoking custom function <custom-function-name>");

        // Pass parameters as query string for functions
        const param1 = Server.Context.QueryParameters["param1"];
        const queryString = "<custom-function-name>(Param1='" + param1 + "')";

        const result = Server.Connector.Dataverse.InvokeCustomApi(
            "GET",
            queryString,
            null
        );

        Server.Logger.Log("<name> custom function completed successfully");

        return JSON.stringify({
            status: "success",
            data: result
        });
    } catch (err) {
        Server.Logger.Error("<name> GET failed: " + err.message);
        return JSON.stringify({
            status: "error",
            message: err.message
        });
    }
}
```

**Key points:**
- **Unbound actions**: Use the action name as the URL, pass parameters as JSON body
- **Entity-bound actions**: Include the entity set and record ID in the URL path, followed by `Microsoft.Dynamics.CRM.<action-name>`
- **Functions (GET)**: Use `"GET"` as the HTTP method and pass parameters inline in the URL using OData function call syntax
- **Actions (POST)**: Use `"POST"` as the HTTP method and pass parameters as JSON body payload
- `InvokeCustomApi` is **synchronous** — do NOT use `async`/`await`
- The server logic can add additional validation, transformation, or logging around the custom action call — it doesn't have to be a pass-through
- When Custom API response properties are known (from Phase 2.1.2), map them to the response object for clarity

#### Dataverse Response Shape (Critical for Frontend Integration)

When a function returns the result of a `Server.Connector.Dataverse.*` method, the client sees a double-wrapped payload — the most common cause of broken frontend integrations. Before writing the function, pick one of three response shapes and record the choice for Phase 9: **Approach A — raw passthrough** (return the connector result as-is), **Approach B — envelope that wraps the connector result** (return `{ status, data: result }` without unwrapping `Body`), or **Approach C — fully normalized** (parse `Body` server-side and return a feature-specific shape — recommended for non-generic endpoints).

See `${CLAUDE_PLUGIN_ROOT}/skills/add-server-logic/references/frontend-integration-reference.md` → "Dataverse Connector Response Format" for the double-wrapping explanation, the `CreateRecord` / `entityid` header behavior, and server- and client-side examples for each shape.

#### Referencing Secrets in Code

When the server logic needs a secret value identified in Phase 2.3, **never hardcode the value**. Instead, read it at runtime from a site setting backed by an environment variable:

```javascript
const apiKey = Server.Sitesetting.Get("ExternalApi/ExchangeRateApiKey");
```

Use the site setting name planned in Phase 2.3. The actual environment variable and site setting YAML will be created in Phase 7.

#### SDK Usage Guidance

Do **not** duplicate Microsoft Learn SDK usage patterns inline in this skill. Use the documentation fetched in Phase 3 as the source of truth for connector methods, signatures, and supported patterns, then apply only the task-specific notes that were captured in the approved plan.

### 5.4 Create the Metadata YAML

For each approved server logic item where the plan status is `create`, generate the metadata file with the deterministic writer script instead of hand-authoring the YAML. The script generates the UUID, writes the fields in the correct order, and returns the created file path as JSON. **Skip this step for `update` / `reuse` items** — the YAML already exists and should be updated manually if needed.

```bash
node "${CLAUDE_PLUGIN_ROOT}/skills/add-server-logic/scripts/create-serverlogic-metadata.js" --projectRoot "<PROJECT_ROOT>" --name "<name>" --displayName "<human-readable display name>" --description "<description of what this server logic does>" --webRoleIds "<uuid1,uuid2,uuid3>"
```

The generated `<PROJECT_ROOT>/.powerpages-site/server-logic/<name>/<name>.serverlogic.yml` file has this structure:

```yaml
adx_serverlogic_adx_webrole:
  - <web-role-guid-1>
  - <web-role-guid-2>
  - <web-role-guid-3>
description: <description of what this server logic does>
display_name: <human-readable display name>
id: <generated-uuid>
name: <name>
```

**Critical requirements:**

- **`id` field is mandatory** — The script generates a new UUID (v4). PAC CLI crashes with `Expected Guid for primary key 'id'` if this is missing.
- **`adx_serverlogic_adx_webrole`** — Array of web role GUIDs from step 5.2. Include only the roles required for that server logic item.
- **`name`** — Must match the folder name and `.js` file name (the URL-friendly name used in `/_api/serverlogics/<name>`).
- **`display_name`** — Human-readable name (e.g., "Exchange Rate API", "Order Processor").
- **Alphabetical field ordering** — Fields must be sorted alphabetically: `adx_serverlogic_adx_webrole`, `description`, `display_name`, `id`, `name`.

### 5.5 Validate the Code

Before saving, verify the code against these constraints:

| Constraint | Check |
|-----------|-------|
| Only allowed top-level functions | No functions other than get, post, put, patch, del |
| Every function returns a string | All code paths return a string (including catch blocks) |
| try/catch in every function | Every function body is wrapped in try/catch |
| Server.Logger in every function | Log at entry, Error in catch |
| No external dependencies | No `import`, `require`, `module.exports` |
| No browser APIs | No `fetch`, `XMLHttpRequest`, `setTimeout`, `console.log`, `document`, `window` |
| Async only when needed | Only functions using `await` are marked `async` |
| ECMAScript 2023 compliant | Standard JS features only (optional chaining, nullish coalescing, etc. are fine) |

### 5.6 Git Commit

After creating the approved server logic files, do a git commit for the server logic changes.

If the HTML plan was generated inside the project, include it in the same commit (use the actual output path from the render script's JSON response).

**Output**: Server logic `.js` and `.serverlogic.yml` files created, validated, and committed

---

## Phase 6: Configure Table Permissions (Conditional)

**Goal**: Set up table permissions for Dataverse tables accessed by `Server.Connector.Dataverse` in the server logic code

**This phase only runs when the server logic uses `Server.Connector.Dataverse`.** If the server logic only uses `Server.Connector.HttpClient` (external APIs) or doesn't access Dataverse at all, skip this phase entirely and proceed to Phase 7.

`Server.Connector.Dataverse` does **NOT** bypass table permissions — it respects them. Without table permissions configured, the Dataverse connector silently returns 0 records instead of the actual data. This is a common source of confusion.

**Actions**:

### 6.1 Detect Dataverse Tables and Required Privileges

Parse the server logic `.js` file created in Phase 5 to identify which Dataverse tables are accessed and what CRUD operations are performed:

| Dataverse SDK Method | Required Table Permission |
|---------------------|--------------------------|
| `RetrieveMultipleRecords("tablename", ...)` | Read |
| `RetrieveRecord("tablename", ...)` | Read |
| `CreateRecord("tablename", ...)` | Create |
| `UpdateRecord("tablename", ...)` | Write |
| `DeleteRecord("tablename", ...)` | Delete |

Extract the entity set name (first argument) from each method call. Build a mapping:

| Table (entity set name) | Read | Create | Write | Delete |
|------------------------|:----:|:------:|:-----:|:------:|
| `accoun

…(truncated)
