diff --git a/.gitignore b/.gitignore index e791bd71..964eb335 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .vscode/launch.json node_modules/ + +# Local auth/token caches and per-folder scratch state - never commit +.local/ diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/README.md b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/README.md new file mode 100644 index 00000000..482a962d --- /dev/null +++ b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/README.md @@ -0,0 +1,261 @@ +# ESS ServiceNow ITSM โ€“ Get Request Details (REQ + RITM) + +## ๐Ÿ“Œ Overview + +Extends the Microsoft Employee Self-Service (ESS) agent with the ability to look up **service catalog requests** (REQ) and **request items** (RITM) from ServiceNow. The built-in ESS ITSM orchestrator only handles incidents (INC), so this sample adds two standalone flows โ€” one for `sc_request`, one for `sc_req_item`. + +A **single user-facing topic** handles both. It extracts the identifier from the user's message and routes RITM-prefixed numbers to the request-item flow and everything else to the request flow. + +**Example conversation:** + +> User: "What's the status of my request REQ0012345?" +> Agent: *[Adaptive card showing request number, state, description, requested for, assigned to, last updated, and a link to open in ServiceNow]* + +> User: "Look up RITM0001234" +> Agent: *[Adaptive card showing request item number, state, catalog item, parent request, assigned to, quantity, and a link to open in ServiceNow]* + +--- + +## ๐Ÿ“ Files + +| File | Purpose | +|------|---------| +| `topics/servicenow-itsm-get-request-details.mcs.yml` | User-facing topic โ€” trigger phrases, AI input extraction, REQ/RITM routing, both adaptive cards | +| `topics/ess-it-servicenow-itsm-system-get-request-details.mcs.yml` | System topic โ€” invokes the REQ flow | +| `topics/ess-it-servicenow-itsm-system-get-request-item.mcs.yml` | System topic โ€” invokes the RITM flow | +| `workflows/get-request-details/` | Power Automate flow โ€” queries `sc_request` by REQ number | +| `workflows/get-request-item/` | Power Automate flow โ€” queries `sc_req_item` by RITM number | +| `solution/ESSServiceNowITSMGetRequestDetails_1_0_0_0.zip` | Unmanaged Power Platform solution containing **both** flows and their ServiceNow connection reference โ€” **Option B** | + +Each `workflows/*` folder ships the flow definition for the push-script path: + +| File | Use it for | +|------|------------| +| `workflow.json` | Raw flow definition โ€” **Option A** (ESS Maker Kit `push.py`) | +| `metadata.yml` | Flow metadata for the ESS Maker Kit push script | + +> **Install all of it.** The user-facing topic references *both* system topics, so the pieces are not independently installable โ€” deploy both flows and all three topics together. + +> **โš ๏ธ Topic schema names depend on how you install.** The user-facing topic reaches the two +> system topics by schema name, e.g. +> `dialog: msdyn_copilotforemployeeselfserviceit.topic.ess-it-servicenow-itsm-system-get-request-item`. +> How that schema name gets assigned differs by install path: +> +> | Install path | Schema name derived from | Result | +> |---|---|---| +> | **Option A** (`push.py`) | the **filename** | `โ€ฆtopic.ess-it-servicenow-itsm-system-get-request-item` | +> | **Option B** (Copilot Studio portal) | the **display name**, spaces removed | `โ€ฆtopic.ESSITServiceNowITSMSystemGetRequestItem` | +> +> The references in this sample are written for **Option A** and resolve as-is. If you install +> via the portal, update both `dialog:` lines in +> `topics/servicenow-itsm-get-request-details.mcs.yml` to the PascalCase form. A mismatch fails +> at publish with `Dialog with id 'โ€ฆ' not found`, followed by a second, misleading +> `IncorrectTypeAssignment` error (`Assigned: Unspecified, expected: String`) โ€” that one is a +> symptom of the unresolved reference, not a separate problem, and clears with it. +> +> Also note the `msdyn_copilotforemployeeselfserviceit` prefix is the **agent's** schema name. +> These topics target the ESS **IT** agent; retarget the prefix if your agent differs. + +--- + +## โœ… Prerequisites + +- Microsoft Employee Self-Service agent (deployed) +- ServiceNow ITSM extension pack installed in Copilot Studio +- An active ServiceNow connection in Power Platform configured for on-behalf-of (invoker) auth โ€” see [Configure OBO](#-configure-obo-required) +- The **`ServiceNowITSMPortalBaseURI`** environment variable populated โ€” see [Set the portal base URI](#-set-the-portal-base-uri-required-for-links) +- [ESS Maker Kit](https://github.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/tree/main/solutions/ess-maker-skills) (for automated deployment via `push.py`) + +--- + +## ๐Ÿš€ Setup + +> **โš ๏ธ OBO (on-behalf-of) is required for this flow.** The ServiceNow connection uses +> `"runtimeSource": "invoker"`, so the flow calls ServiceNow **as the signed-in employee**, +> not as the maker. This is what keeps ServiceNow's own row-level ACLs in force โ€” each user +> only sees requests they're entitled to. See [Configure OBO](#-configure-obo-required) below. + +> **๐Ÿ”— Also required:** set the `ServiceNowITSMPortalBaseURI` environment variable. +> It ships empty, and the "Open in ServiceNow" links render without a host until you set it. +> See [Set the portal base URI](#-set-the-portal-base-uri-required-for-links) below. + +### Option A โ€” [ESS Maker Kit](https://github.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/tree/main/solutions/ess-maker-skills) (recommended) + +This sample ships **two** flows. Generate a separate GUID for each and repeat the copy/replace for both. + +1. Run `/setup` in your ESS Maker Kit workspace to connect to the environment. +2. Copy `topics/` โ†’ `workspace/agents/{slug}/topics/` (all three topic files). +3. Copy each flow into its own workspace folder: + - `workflows/get-request-details/` โ†’ `workspace/agents/{slug}/workflows/ess-it-servicenow-itsm-get-request-details-{GUID-A}/` + - `workflows/get-request-item/` โ†’ `workspace/agents/{slug}/workflows/ess-it-servicenow-itsm-get-request-item-{GUID-B}/` +4. Replace the placeholders **in each flow folder and its matching system topic**: + - `{FLOW_GUID}` โ†’ `{GUID-A}` in `ess-it-servicenow-itsm-system-get-request-details.mcs.yml` and `get-request-details/metadata.yml`. + - `{FLOW_GUID}` โ†’ `{GUID-B}` in `ess-it-servicenow-itsm-system-get-request-item.mcs.yml` and `get-request-item/metadata.yml`. + - `{SERVICENOW_CONNREF}` / `{DATAVERSE_CONNREF}` โ€” in both `workflow.json` files, from `workspace/agents/{slug}/connectionreferences.mcs.yml`. +5. `python scripts/push.py` +6. **Configure OBO** โ€” see below. Do this *before* turning the flows on. +7. Turn **both flows** on in [Power Automate](https://make.powerautomate.com) (API activation is blocked for invoker-auth ServiceNow flows). +8. `python scripts/push.py --repair "Get Request Details"` and `--repair "Get Request Item"` to wire topic โ†’ flow. +9. Publish the agent in Copilot Studio. + +### Option B โ€” Import the solution + +The solution ships both flows plus its own ServiceNow connection reference +(`esss_servicenow`), so there is exactly one connection to map at import time. + +#### B1 โ€” Import the solution + +1. Go to [Power Apps](https://make.powerapps.com) and confirm the environment picker (top right) shows your target environment. +2. Select **Solutions** in the left nav, then **Import solution** on the command bar. +3. Select **Browse**, choose `solution/ESSServiceNowITSMGetRequestDetails_1_0_0_0.zip`, then **Next**. +4. On the **Connections** step, map **ESS Sample ServiceNow Connection** to your ServiceNow connection. If you don't have one, select **+ New connection**, create it, then return and select **Refresh**. + - You will **not** be asked to map a Dataverse connection. The flows bind Dataverse through `new_sharedcommondataserviceforapps_41c83`, which is already in your environment courtesy of the ServiceNow extension pack. +5. Select **Import** and wait for the success banner. + +#### B2 โ€” Configure OBO (do this before testing) + +Both flows call ServiceNow **as the signed-in employee**, which requires parameter sharing on the +ServiceNow connection. This is configured in **Copilot Studio**, not Power Automate โ€” follow +[Configure OBO](#-configure-obo-required) below, then return here. + +#### B3 โ€” Collect the two flow IDs + +1. In [Power Automate](https://make.powerautomate.com), select **Solutions**, then open **ESS ServiceNow ITSM - Get Request Details**. +2. Confirm both flows show **Status: On**. If either is **Off**, open it and select **Turn on**. + - `ESS IT ServiceNow ITSM Get Request Details` + - `ESS IT ServiceNow ITSM Get Request Item` +3. Open each flow and read its ID from the browser address bar โ€” it's the GUID immediately after `/flows/`: + + ```text + https://make.powerautomate.com/environments/{env}/solutions/{sol}/flows/5750fc72-738f-412c-b9cb-5094507f2eb2/details + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ flow ID โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + ``` + + Record one ID per flow โ€” they are different values. Keep them straight by flow name: swapping them sends REQ lookups to the RITM flow and vice versa, which fails silently with an empty card rather than an error. + +#### B4 โ€” Create the three topics + +For each file in `topics/`: + +1. In [Copilot Studio](https://copilotstudio.microsoft.com/), open your agent and select **Topics** โ†’ **+ Add a topic** โ†’ **From blank**. +2. Open the **โ‹ฎ** menu and select **Open code editor**. +3. Paste the entire contents of the `.mcs.yml` file, replacing whatever is already there. +4. Set the topic name **exactly** as below. The name matters: the portal derives the topic's schema name from it, and the user-facing topic locates the system topics by schema name. + + | File | Topic name to use | + |------|-------------------| + | `ess-it-servicenow-itsm-system-get-request-details.mcs.yml` | `ESS IT ServiceNow ITSM System Get Request Details` | + | `ess-it-servicenow-itsm-system-get-request-item.mcs.yml` | `ESS IT ServiceNow ITSM System Get Request Item` | + | `servicenow-itsm-get-request-details.mcs.yml` | `ESS ServiceNow ITSM Get Request Details` | + +5. In each **system** topic, replace `{FLOW_GUID}` with that topic's matching flow ID from B3: + - `โ€ฆsystem-get-request-details` โ†’ ID of **Get Request Details** + - `โ€ฆsystem-get-request-item` โ†’ ID of **Get Request Item** +6. In the **user-facing** topic, replace both `dialog:` values with the PascalCase schema names the portal generated: + + ```yaml + dialog: msdyn_copilotforemployeeselfserviceit.topic.ESSITServiceNowITSMSystemGetRequestDetails + dialog: msdyn_copilotforemployeeselfserviceit.topic.ESSITServiceNowITSMSystemGetRequestItem + ``` + + The shipped values are kebab-case (correct for Option A only). Leaving them produces + `Dialog with id 'โ€ฆ' not found` at publish โ€” see the note under [Files](#-files). +7. **Save** each topic. + +#### B5 โ€” Publish + +Select **Publish** in Copilot Studio. If publish reports `Dialog with id โ€ฆ not found`, revisit +step B4.6; if it reports `IncorrectTypeAssignment` alongside it, that's the same fault, not a +second one. + +> The solution declares the ServiceNow connection as `"runtimeSource": "invoker"`, so an imported +> flow is wired for OBO from the start. You still need the connection-side step in B2. + +### ๐Ÿ”— Set the portal base URI (required for links) + +Both flows in this sample resolve the "Open in ServiceNow" link base the same way the +out-of-the-box ESS orchestrator does: + +1. the **`ServiceNowITSMPortalBaseURI`** environment variable's **current value**, then +2. that variable's **default value**, then +3. the `ServiceNowPortalBaseURI` property of the `msdyn_ServiceNowITSM` template + configuration record. + +**This environment variable ships empty**, and so does the template configuration +property. If you don't set one of them, the adaptive cards still render correctly, +but every "Open in ServiceNow" link points at `?id=ticket&table=...` with no host +and won't resolve. + +1. Go to [make.powerapps.com](https://make.powerapps.com) โ†’ your environment โ†’ + **Solutions** โ†’ **Default Solution** โ†’ **Environment variables**. +2. Open **ServiceNowITSMPortalBaseURI**. +3. Set the **Current value** to your ServiceNow portal base URL โ€” no trailing + slash, no query string: + + ```text + https://.service-now.com/esc + ``` + + Use `/esc` for **Employee Center** or `/sp` for the classic **Service Portal**, + whichever your employees use. To confirm which is default in your instance, go + to **Service Portal โ†’ Portals** in ServiceNow and check the **URL suffix** of + the portal flagged as default. +4. **Save**. + +The topics append `?id=ticket&table=sc_request&sys_id=...` to this value, so a +finished link looks like: + +```text +https://.service-now.com/esc?id=ticket&table=sc_request&sys_id=127a3aef8356cf1028e6cc65eeaad3d4 +``` + +> This variable is shared with the out-of-the-box ServiceNow ITSM topics โ€” +> setting it fixes their links too. + +--- + +### ๐Ÿ” Configure OBO (required) + +Both flows must call ServiceNow as the signed-in employee. Enable parameter sharing on the +ServiceNow connection backing **each** flow: + +1. In [Copilot Studio](https://copilotstudio.microsoft.com/), select **Agents** and open your agent. +2. Select **Settings**, then **Connection Settings**. +3. Find the **ServiceNow** connection row. Under **Manage**, select **See details**. +4. Open the **Connection parameters** tab. +5. Turn on **Allow permission to share parameters**. +6. Select the checkboxes for the parameters you want the user to share. +7. Repeat for the connection entry of **each** flow (Get Request Details *and* Get Request Item). + +Each employee is prompted to grant permission the first time the agent uses the connection on +their behalf. Full reference: +[Share connection parameters for On-Behalf-Of (OBO) authentication](https://learn.microsoft.com/en-us/microsoft-copilot-studio/authoring-connections#share-connection-parameters-for-on-behalf-of-obo-authentication). + +You also need the ServiceNow side to permit those users โ€” the OAuth application registry entry +must allow your Entra ID users, and they need read access to `sc_request` and `sc_req_item`. + +Confirm it worked: both `workflow.json` files should show `"runtimeSource": "invoker"` on the +ServiceNow connection reference after deployment. If either flips to `"embedded"`, that flow is +running as the maker and every user will see the maker's data โ€” reconfigure before shipping. + +> **Note:** configuring OBO through the Copilot Studio UI rewrites the flow's +> `connectionReferences` block (the connection key typically becomes `shared_service-now-1`). +> If you later re-export or re-push the flow, re-check that block so you don't overwrite the +> live OBO binding with a stale one. + +--- + +## โš ๏ธ Known Limitations + +- **Activation via API fails** โ€” when deploying via the Maker Kit push script, the flows are created but cannot be activated automatically. Power Platform validates the ServiceNow connection schema at activation time, which requires a live user session. Manual turn-on in Power Automate is required (step 7 above). +- **sc_request fields** โ€” the REQ flow returns `request_state` (not `state`) and `requested_for` (not `caller_id`). The adaptive card handles this, but if you customise the output be aware the field names differ from the incident table. +- **Reference fields** โ€” `requested_for`, `assigned_to`, `cat_item`, and `request` come back from `GetRecords` as `{display_value, link}` objects when populated (or an empty string when not set). Each flow's `Flatten_Reference_Fields` step normalizes these to plain display-value strings before mapping โ€” if you add more reference fields to `sysparm_fields`, apply the same flatten pattern. +- **Result key** โ€” the ServiceNow connector returns rows under `result` (ServiceNow Table API v2), *not* OData's `value`. Feeding `body(...)?['value']` to the output mapper yields `null` and fails with `A null value was found for the property named 'ServiceNowTableData'`. Keep the `?['result']` references intact if you customise these flows. + +--- + +## Author + +| Author | Original Publish Date | Latest Publish Date | +| --- | --- | --- | +| Dean Cron | 2026-07-30 | 2026-08-03 | diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/solution/ESSServiceNowITSMGetRequestDetails_1_0_0_0.zip b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/solution/ESSServiceNowITSMGetRequestDetails_1_0_0_0.zip new file mode 100644 index 00000000..46c90a24 Binary files /dev/null and b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/solution/ESSServiceNowITSMGetRequestDetails_1_0_0_0.zip differ diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/topics/ess-it-servicenow-itsm-system-get-request-details.mcs.yml b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/topics/ess-it-servicenow-itsm-system-get-request-details.mcs.yml new file mode 100644 index 00000000..37b61457 --- /dev/null +++ b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/topics/ess-it-servicenow-itsm-system-get-request-details.mcs.yml @@ -0,0 +1,40 @@ +kind: AdaptiveDialog +inputs: + - kind: ManualTaskInput + propertyName: SysId + value: ="" + +beginDialog: + kind: OnRedirect + id: main + actions: + - kind: InvokeFlowAction + id: callGetRequestDetailsFlow + input: + binding: + text: =Topic.SysId + + output: + binding: + errormessage: Topic.ErrorMessage + issuccessful: Topic.IsSuccessful + servicenowdata: Topic.ServiceNowData + servicenowsessionid: Topic.ServiceNowSessionID + servicenowtransactionid: Topic.ServiceNowTransactionID + statuscode: Topic.StatusCode + + # Replace {FLOW_GUID} with the workflowId from workflow/metadata.yml + flowId: "{FLOW_GUID}" + +inputType: + properties: + SysId: + displayName: SysId + type: String + +outputType: + properties: + ServiceNowData: + displayName: ServiceNowData + description: Request details data returned from ServiceNow. + type: String diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/topics/ess-it-servicenow-itsm-system-get-request-item.mcs.yml b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/topics/ess-it-servicenow-itsm-system-get-request-item.mcs.yml new file mode 100644 index 00000000..323f9ddc --- /dev/null +++ b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/topics/ess-it-servicenow-itsm-system-get-request-item.mcs.yml @@ -0,0 +1,38 @@ +kind: AdaptiveDialog +inputs: + - kind: ManualTaskInput + propertyName: SysId + value: ="" + +beginDialog: + kind: OnRedirect + id: main + actions: + - kind: InvokeFlowAction + id: callGetRequestItemFlow + input: + binding: + text: =Topic.SysId + output: + binding: + errormessage: Topic.ErrorMessage + issuccessful: Topic.IsSuccessful + servicenowdata: Topic.ServiceNowData + servicenowsessionid: Topic.ServiceNowSessionID + servicenowtransactionid: Topic.ServiceNowTransactionID + statuscode: Topic.StatusCode + # Replace {FLOW_GUID} with the workflowId from workflow/metadata.yml + flowId: "{FLOW_GUID}" + +inputType: + properties: + SysId: + displayName: SysId + type: String + +outputType: + properties: + ServiceNowData: + displayName: ServiceNowData + description: Request item details data returned from ServiceNow. + type: String diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/topics/servicenow-itsm-get-request-details.mcs.yml b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/topics/servicenow-itsm-get-request-details.mcs.yml new file mode 100644 index 00000000..1e7d93b0 --- /dev/null +++ b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/topics/servicenow-itsm-get-request-details.mcs.yml @@ -0,0 +1,414 @@ +kind: AdaptiveDialog +inputs: + - kind: ManualTaskInput + propertyName: IconBase64 + value: data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPG1hc2sgaWQ9Im1hc2swXzM5NzNfMTkwMTU4IiBzdHlsZT0ibWFzay10eXBlOmx1bWluYW5jZSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIj4KPHBhdGggZD0iTTYgNEM0Ljg5NTQzIDQgNCA0Ljg5NTQzIDQgNlYxNEM0IDE1LjEwNDYgNC44OTU0MyAxNiA2IDE2SDE0QzE1LjEwNDYgMTYgMTYgMTUuMTA0NiAxNiAxNFYxMS41QzE2IDExLjIyMzkgMTYuMjIzOSAxMSAxNi41IDExQzE2Ljc3NjEgMTEgMTcgMTEuMjIzOSAxNyAxMS41VjE0QzE3IDE1LjY1NjkgMTUuNjU2OSAxNyAxNCAxN0g2QzQuMzQzMTUgMTcgMyAxNS42NTY5IDMgMTRWNkMzIDQuMzQzMTUgNC4zNDMxNSAzIDYgM0g4LjVDOC43NzYxNCAzIDkgMy4yMjM4NiA5IDMuNUM5IDMuNzc2MTQgOC43NzYxNCA0IDguNSA0SDZaTTExIDMuNUMxMSAzLjIyMzg2IDExLjIyMzkgMyAxMS41IDNIMTYuNUMxNi43NzYxIDMgMTcgMy4yMjM4NiAxNyAzLjVWOC41QzE3IDguNzc2MTQgMTYuNzc2MSA5IDE2LjUgOUMxNi4yMjM5IDkgMTYgOC43NzYxNCAxNiA4LjVWNC43MDcxMUwxMS44NTM2IDguODUzNTVDMTEuNjU4MyA5LjA0ODgyIDExLjM0MTcgOS4wNDg4MiAxMS4xNDY0IDguODUzNTVDMTAuOTUxMiA4LjY1ODI5IDEwLjk1MTIgOC4zNDE3MSAxMS4xNDY0IDguMTQ2NDVMMTUuMjkyOSA0SDExLjVDMTEuMjIzOSA0IDExIDMuNzc2MTQgMTEgMy41WiIgZmlsbD0id2hpdGUiLz4KPC9tYXNrPgo8ZyBtYXNrPSJ1cmwoI21hc2swXzM5NzNfMTkwMTU4KSI+CjxyZWN0IHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iIzQ2NEZFQiIvPgo8L2c+Cjwvc3ZnPgo= + + - kind: AutomaticTaskInput + propertyName: RequestIdentifier + description: |- + `RequestIdentifier` is a unique value used to identify a service catalog request or request item. + + There are three valid types of RequestIdentifiers: + 1. **SysID** + - A globally unique 32-character identifier. + - Format: hexadecimal string (digits 0-9 and lowercase letters a-f). + - Regex: `^[a-f0-9]{32}$` + 2. **RequestNumber (REQ)** + - Format: Always starts with the prefix `REQ` followed by digits. + - Regex: `^REQ\d{5,}$` + 3. **RequestItemNumber (RITM)** + - Format: Always starts with the prefix `RITM` followed by digits. + - Regex: `^RITM\d{5,}$` + + If neither identifier is available, ask the user to provide their REQ or RITM number. + + **Important:** The extracted `SysID` or `RequestNumber` must be an exact match with the user query. Partial or fuzzy matches are not valid. + entity: StringPrebuiltEntity + shouldPromptUser: false + + - kind: AutomaticTaskInput + propertyName: RelevantRequestFound + description: |- + `RelevantRequestFound` is a boolean value that indicates whether a matching request was found based on the user's input. + + Set `RelevantRequestFound = true` **only if** one of the following conditions is met: + 1. **Exact Identifier Match** + - The user provides a `RequestNumber` (starts with `"REQ"`), a `RequestItemNumber` (starts with `"RITM"`), or a `SysID` (32-character hexadecimal string). + + 2. **High-Relevance Descriptive Match** + - The user describes a specific request with VERY HIGH SEMANTIC RELEVANCE. + - If the match is partial, uncertain, or low-confidence, set `RelevantRequestFound = false`. + + **Do not set `RelevantRequestFound = true` for partial, fuzzy, or ambiguous matches.** + entity: BooleanPrebuiltEntity + shouldPromptUser: false + +modelDescription: |- + Use this tool when a user asks for details about a specific service catalog request (REQ) or request item (RITM). + + This tool retrieves details for a REQ (service catalog request) or RITM (request item) identified by its number or SysId. + + Do NOT use this tool for incident tickets (which start with INC). Use ServiceNow-ITSM-Get-Ticket-Details for incidents instead. + + Example queries handled by this tool: + * "What's the status of my request REQ0012345?" + * "Show me details of REQ0067890" + * "Look up RITM0001234" + * "What's happening with RITM0005678?" + * "Where is my service request?" + * "Has my software request been approved?" + * "Check the status of my access request" +beginDialog: + kind: OnRecognizedIntent + id: main + intent: {} + actions: + - kind: ConditionGroup + id: conditionGroup_checkRequest + conditions: + - id: conditionItem_requestFound + condition: =Topic.RelevantRequestFound = true + displayName: Is a relevant request found? + actions: + - kind: ConditionGroup + id: conditionGroup_ritmOrReq + conditions: + - id: conditionItem_isRitm + condition: =StartsWith(Upper(Topic.RequestIdentifier), "RITM") + displayName: Is this a RITM (request item)? + actions: + # Requires the companion "servicenow-itsm-get-request-item" sample to be installed + # (provides the ess-it-servicenow-itsm-system-get-request-item system topic + flow). + - kind: BeginDialog + id: callSystemGetRequestItem + input: + binding: + SysId: =Topic.RequestIdentifier + + dialog: msdyn_copilotforemployeeselfserviceit.topic.ess-it-servicenow-itsm-system-get-request-item + output: + binding: + ServiceNowData: Topic.ServiceNowData + + - kind: ConditionGroup + id: conditionGroup_ritmDataFound + conditions: + - id: conditionItem_ritmHasData + condition: =!IsBlank(Topic.ServiceNowData) + displayName: Was request item data returned? + actions: + - kind: ParseValue + id: parseRitmDetails + variable: Topic.RitmDetails + valueType: + kind: Record + properties: + AssignedTo: + type: String + examples: + CatalogItem: + type: String + examples: + Description: + type: String + examples: + LastUpdated: + type: String + examples: + ParentRequest: + type: String + examples: + Quantity: + type: String + examples: + RequestItemNumber: + type: String + examples: + ServiceNowPortalBaseURI: + type: String + examples: + ShortDescription: + type: String + examples: + State: + type: String + examples: + SysId: + type: String + examples: + + value: =Topic.ServiceNowData + + - kind: SendActivity + id: sendRitmCard + activity: + text: + - You can view the request item details below. + attachments: + - kind: AdaptiveCardTemplate + cardContent: |- + ={ + type: "AdaptiveCard", + '$schema': "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.3", + body: [ + { + type: "Container", + items: [ + { + type: "RichTextBlock", + inlines: [ + {type: "TextRun", text: "Request Item " & Topic.RitmDetails.RequestItemNumber & " | ", weight: "Bolder"}, + {type: "TextRun", text: Topic.RitmDetails.State, weight: "Bolder", + color: If(Lower(Topic.RitmDetails.State) = "approved", "good", "default")} + ] + }, + {type: "TextBlock", text: Topic.RitmDetails.ShortDescription, wrap: true, weight: "Bolder", size: "Medium"}, + { + type: "ColumnSet", + columns: [ + {type: "Column", width: "50", items: [ + {type: "TextBlock", text: "Catalog item", isSubtle: true, wrap: true}, + {type: "TextBlock", text: If(IsBlank(Topic.RitmDetails.CatalogItem), "โ€”", Topic.RitmDetails.CatalogItem), wrap: true, spacing: "None"} + ]}, + {type: "Column", width: "50", items: [ + {type: "TextBlock", text: "Parent request", isSubtle: true, wrap: true}, + {type: "TextBlock", text: If(IsBlank(Topic.RitmDetails.ParentRequest), "โ€”", Topic.RitmDetails.ParentRequest), wrap: true, spacing: "None"} + ]} + ] + }, + { + type: "ColumnSet", + columns: [ + {type: "Column", width: "50", items: [ + {type: "TextBlock", text: "Assigned to", isSubtle: true, wrap: true}, + {type: "TextBlock", text: If(IsBlank(Topic.RitmDetails.AssignedTo), "No agent assigned", Topic.RitmDetails.AssignedTo), wrap: true, spacing: "None"} + ]}, + {type: "Column", width: "50", items: [ + {type: "TextBlock", text: "Quantity", isSubtle: true, wrap: true}, + {type: "TextBlock", text: If(IsBlank(Topic.RitmDetails.Quantity), "1", Topic.RitmDetails.Quantity), wrap: true, spacing: "None"} + ]} + ] + }, + {type: "TextBlock", text: "Last updated", isSubtle: true, wrap: true}, + {type: "TextBlock", text: Text(DateValue(Topic.RitmDetails.LastUpdated), "mmmm d, yyyy"), wrap: true, spacing: "None"} + ] + }, + { + type: "Container", + separator: true, + spacing: "ExtraLarge", + items: [{ + type: "ColumnSet", + columns: [ + {type: "Column", width: "auto", items: [{ + type: "TextBlock", + text: "[Open in ServiceNow](" & Topic.RitmDetails.ServiceNowPortalBaseURI & "?id=ticket&table=sc_req_item&sys_id=" & Topic.RitmDetails.SysId & ")", + wrap: true, weight: "Bolder" + }]}, + {type: "Column", width: "auto", + selectAction: {type: "Action.OpenUrl", url: Topic.RitmDetails.ServiceNowPortalBaseURI & "?id=ticket&table=sc_req_item&sys_id=" & Topic.RitmDetails.SysId}, + items: [{type: "Image", url: Topic.IconBase64}]} + ] + }] + } + ] + } + + - kind: CancelAllDialogs + id: cancelAfterRitm + + elseActions: + - kind: SendActivity + id: sendRitmNotFound + activity: + text: + - I couldn't find a request item matching that RITM number. Please check the number and try again. + + - kind: CancelAllDialogs + id: cancelAfterRitmNotFound + + elseActions: + - kind: BeginDialog + id: callSystemGetRequestDetails + input: + binding: + SysId: =Topic.RequestIdentifier + + dialog: msdyn_copilotforemployeeselfserviceit.topic.ess-it-servicenow-itsm-system-get-request-details + output: + binding: + ServiceNowData: Topic.ServiceNowData + + + - kind: ConditionGroup + id: conditionGroup_dataFound + conditions: + - id: conditionItem_hasData + condition: =!IsBlank(Topic.ServiceNowData) && !StartsWith(Upper(Topic.RequestIdentifier), "RITM") + displayName: Was request data returned? + actions: + - kind: ParseValue + id: parseRequestDetails + variable: Topic.RequestDetails + valueType: + kind: Record + properties: + AssignedTo: + type: String + examples: + + Comments: + type: String + examples: + + Description: + type: String + examples: + + LastUpdated: + type: String + examples: + + RequestedFor: + type: String + examples: + + RequestNumber: + type: String + examples: + + ServiceNowPortalBaseURI: + type: String + examples: + + ShortDescription: + type: String + examples: + + State: + type: String + examples: + + SysId: + type: String + examples: + + value: =Topic.ServiceNowData + + - kind: SetVariable + id: setFirstComment + variable: Topic.FirstComment + value: =First(Split(Topic.RequestDetails.Comments, Char(10) & Char(10))).Value + + - kind: SetTextVariable + id: setRequestTitle + variable: Topic.RequestTitle + value: "Service Request #{Topic.RequestDetails.RequestNumber} | " + + - kind: SendActivity + id: sendRequestCard + activity: + text: + - You can view the request details below. + attachments: + - kind: AdaptiveCardTemplate + cardContent: |- + ={ + type: "AdaptiveCard", + '$schema': "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.3", + body: [ + { + type: "Container", + items: [ + { + type: "RichTextBlock", + inlines: [ + {type: "TextRun", text: Topic.RequestTitle, weight: "Bolder"}, + {type: "TextRun", text: Topic.RequestDetails.State, color: If(Lower(Topic.RequestDetails.State) = "approved", "good", "default"), weight: "Bolder"} + ] + }, + {type: "TextBlock", text: Topic.RequestDetails.ShortDescription, wrap: true, weight: "Bolder", size: "Medium"}, + { + type: "ColumnSet", + columns: [ + {type: "Column", width: "50", items: [{type: "TextBlock", text: "Requested for", isSubtle: true, wrap: true}, {type: "TextBlock", text: If(IsBlank(Topic.RequestDetails.RequestedFor), "โ€”", Topic.RequestDetails.RequestedFor), wrap: true, spacing: "None"}]}, + {type: "Column", width: "50", items: [{type: "TextBlock", text: "Assigned to", isSubtle: true, wrap: true}, {type: "TextBlock", text: If(IsBlank(Topic.RequestDetails.AssignedTo), "No agent assigned", Topic.RequestDetails.AssignedTo), wrap: true, spacing: "None"}]} + ] + }, + {type: "ColumnSet", columns: [{type: "Column", width: "100", items: [{type: "TextBlock", text: "Last updated", wrap: true, isSubtle: true}, {type: "TextBlock", text: Text(DateValue(Topic.RequestDetails.LastUpdated), "mmmm d, yyyy"), wrap: true, spacing: "None"}]}]}, + If(IsBlank(Topic.RequestDetails.Description),{},{type: "TextBlock", text: "Description", wrap: true, isSubtle: true}), + If(IsBlank(Topic.RequestDetails.Description),{},{type: "TextBlock", text: Topic.RequestDetails.Description, wrap: true, spacing: "None"}), + If(IsBlank(Topic.RequestDetails.Comments),{},{type: "TextBlock", text: "Latest comment", wrap: true, isSubtle: true}), + If(IsBlank(Topic.RequestDetails.Comments),{},{type: "TextBlock", text: Mid(Topic.FirstComment, Find(Char(10), Topic.FirstComment) + 1, Len(Topic.FirstComment)), wrap: true, spacing: "None"}), + {type: "TextBlock", text: "SysId", isVisible: false}, + {type: "TextBlock", text: Topic.RequestDetails.SysId, isVisible: false} + ] + }, + { + type: "Container", + separator: true, + spacing: "ExtraLarge", + items: [ + { + type: "ColumnSet", + columns: [ + {type: "Column", width: "auto", items: [{type: "TextBlock", text: "[Open in ServiceNow](" & Topic.RequestDetails.ServiceNowPortalBaseURI & "?id=ticket&table=sc_request&sys_id=" & Topic.RequestDetails.SysId & ")", wrap: true, weight: "Bolder"}]}, + {type: "Column", width: "auto", selectAction: {type: "Action.OpenUrl", url: Topic.RequestDetails.ServiceNowPortalBaseURI & "?id=ticket&table=sc_request&sys_id=" & Topic.RequestDetails.SysId}, items: [{type: "Image", url: Topic.IconBase64}]} + ] + } + ] + } + ] + } + + - kind: CancelAllDialogs + id: cancelAllDialogs + + elseActions: + - kind: ConditionGroup + id: conditionGroup_reqNotFoundGuard + conditions: + - id: conditionItem_notRitm + condition: =!StartsWith(Upper(Topic.RequestIdentifier), "RITM") + displayName: Not a RITM โ€” show REQ not found + actions: + - kind: SendActivity + id: sendNotFound + activity: + text: + - I couldn't find a service request matching that number. Please check the REQ number and try again. + elseActions: [] + + elseActions: + - kind: SetVariable + id: setNotFound + variable: Topic.RelevantRequestFound + value: false + +inputType: + properties: + IconBase64: + displayName: IconBase64 + type: String + examples: + + RelevantRequestFound: + displayName: RelevantRequestFound + type: Boolean + examples: + + RequestIdentifier: + displayName: RequestIdentifier + description: REQ or RITM number, or SysId, of the service catalog request/item. + type: String + examples: + +outputType: + properties: + RelevantRequestFound: + displayName: RelevantRequestFound + type: Boolean + examples: diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/workflows/get-request-details/metadata.yml b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/workflows/get-request-details/metadata.yml new file mode 100644 index 00000000..6e00b208 --- /dev/null +++ b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/workflows/get-request-details/metadata.yml @@ -0,0 +1,16 @@ +# Replace {FLOW_GUID} with a newly generated GUID (e.g. from: python -c "import uuid; print(uuid.uuid4())") +# Replace {SERVICENOW_CONNREF} with your agent's ServiceNow connection reference logical name +# Replace {DATAVERSE_CONNREF} with your agent's Dataverse connection reference logical name +# Both connrefs can be found in workspace/agents/{slug}/connectionreferences.mcs.yml +jsonFileName: workflow/workflow.json +workflowId: "{FLOW_GUID}" +name: ESS IT ServiceNow ITSM Get Request Details +type: 1 +description: "Retrieves details for a specific ServiceNow service catalog request (sc_request table) by REQ number." +subprocess: false +category: 5 +mode: 0 +scope: 4 +stateCode: 1 +statusCode: 2 +isTransacted: true diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/workflows/get-request-details/workflow.json b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/workflows/get-request-details/workflow.json new file mode 100644 index 00000000..ec778f3c --- /dev/null +++ b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/workflows/get-request-details/workflow.json @@ -0,0 +1,313 @@ +{ + "schemaVersion": "1.0.0.0", + "properties": { + "connectionReferences": { + "shared_service-now": { + "api": { + "name": "shared_service-now" + }, + "connection": { + "connectionReferenceLogicalName": "{SERVICENOW_CONNREF}" + }, + "runtimeSource": "invoker" + }, + "shared_commondataserviceforapps": { + "api": { + "name": "shared_commondataserviceforapps" + }, + "connection": { + "connectionReferenceLogicalName": "{DATAVERSE_CONNREF}" + }, + "runtimeSource": "embedded" + } + }, + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "$authentication": { + "defaultValue": {}, + "type": "SecureObject" + }, + "$connections": { + "defaultValue": {}, + "type": "Object" + } + }, + "triggers": { + "manual": { + "type": "Request", + "kind": "Skills", + "inputs": { + "schema": { + "type": "object", + "properties": { + "text": { + "description": "REQ number (e.g. REQ0012345) of the service catalog request to retrieve.", + "title": "SysId", + "type": "string", + "x-ms-content-hint": "TEXT", + "x-ms-dynamically-added": true + } + }, + "required": [ + "text" + ] + } + } + } + }, + "actions": { + "Initialize_variable_-_ResponseBody": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "ResponseBody", + "type": "object" + } + ] + }, + "runAfter": {} + }, + "Retrieve_Base_ITSM_Config": { + "type": "OpenApiConnection", + "inputs": { + "parameters": { + "entityName": "msdyn_employeeselfservicetemplateconfigs", + "$select": "msdyn_value", + "$filter": "msdyn_uniquename eq 'msdyn_ServiceNowITSM'", + "$top": 1 + }, + "host": { + "apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps", + "operationId": "ListRecords", + "connectionName": "shared_commondataserviceforapps" + } + }, + "runAfter": { + "Initialize_variable_-_ResponseBody": [ + "Succeeded" + ] + } + }, + "ServiceNow_Get_Request": { + "type": "OpenApiConnection", + "inputs": { + "parameters": { + "tableType": "sc_request", + "sysparm_query": "@concat('number=', triggerBody()?['text'])", + "sysparm_limit": 1, + "sysparm_display_value": true, + "sysparm_exclude_reference_link": false, + "sysparm_fields": "number,short_description,description,request_state,requested_for,assigned_to,comments,sys_updated_on,sys_id" + }, + "host": { + "apiId": "/providers/Microsoft.PowerApps/apis/shared_service-now", + "operationId": "GetRecords", + "connectionName": "shared_service-now" + } + }, + "runAfter": { + "Retrieve_ServiceNow_ITSM_Portal_Base_URI": [ + "Succeeded" + ] + }, + "limit": { + "timeout": "PT1M" + } + }, + "Condition_-_ServiceNow_call_succeeded": { + "type": "If", + "expression": { + "and": [ + { + "greater": [ + "@length(coalesce(body('ServiceNow_Get_Request')?['result'], json('[]')))", + 0 + ] + } + ] + }, + "actions": { + "Store_First_Result": { + "type": "Compose", + "inputs": "@first(body('ServiceNow_Get_Request')?['result'])", + "runAfter": {} + }, + "Flatten_Reference_Fields": { + "type": "Compose", + "inputs": { + "number": "@outputs('Store_First_Result')?['number']", + "short_description": "@outputs('Store_First_Result')?['short_description']", + "description": "@outputs('Store_First_Result')?['description']", + "request_state": "@outputs('Store_First_Result')?['request_state']", + "requested_for": "@{if(startsWith(string(outputs('Store_First_Result')?['requested_for']), '{'), json(string(outputs('Store_First_Result')?['requested_for']))?['display_value'], string(outputs('Store_First_Result')?['requested_for']))}", + "assigned_to": "@{if(startsWith(string(outputs('Store_First_Result')?['assigned_to']), '{'), json(string(outputs('Store_First_Result')?['assigned_to']))?['display_value'], string(outputs('Store_First_Result')?['assigned_to']))}", + "comments": "@outputs('Store_First_Result')?['comments']", + "sys_updated_on": "@outputs('Store_First_Result')?['sys_updated_on']", + "sys_id": "@outputs('Store_First_Result')?['sys_id']" + }, + "runAfter": { + "Store_First_Result": [ + "Succeeded" + ] + } + }, + "Map_output_fields": { + "type": "OpenApiConnection", + "inputs": { + "parameters": { + "actionName": "msdyn_ServiceNowOutputFieldMapperPlugin", + "item/ServiceNowTableData": "@outputs('Flatten_Reference_Fields')", + "item/OutputFieldMapping": "[{\"FieldName\":\"number\",\"OutputName\":\"RequestNumber\"},{\"FieldName\":\"short_description\",\"OutputName\":\"ShortDescription\"},{\"FieldName\":\"description\",\"OutputName\":\"Description\"},{\"FieldName\":\"request_state\",\"OutputName\":\"State\"},{\"FieldName\":\"requested_for\",\"OutputName\":\"RequestedFor\"},{\"FieldName\":\"assigned_to\",\"OutputName\":\"AssignedTo\"},{\"FieldName\":\"comments\",\"OutputName\":\"Comments\"},{\"FieldName\":\"sys_updated_on\",\"OutputName\":\"LastUpdated\"},{\"FieldName\":\"sys_id\",\"OutputName\":\"SysId\"}]", + "item/ServiceNowITSMBaseConfiguration": "@setProperty(json(coalesce(first(outputs('Retrieve_Base_ITSM_Config')?['body/value'])?['msdyn_value'], '{}')), 'ServiceNowPortalBaseURI', if(not(empty(coalesce(first(first(outputs('Retrieve_ServiceNow_ITSM_Portal_Base_URI')?['body/value'])?['environmentvariabledefinition_environmentvariablevalue'])?['value'], ''))), first(first(outputs('Retrieve_ServiceNow_ITSM_Portal_Base_URI')?['body/value'])?['environmentvariabledefinition_environmentvariablevalue'])?['value'], if(not(empty(coalesce(first(outputs('Retrieve_ServiceNow_ITSM_Portal_Base_URI')?['body/value'])?['defaultvalue'], ''))), first(outputs('Retrieve_ServiceNow_ITSM_Portal_Base_URI')?['body/value'])?['defaultvalue'], json(coalesce(first(outputs('Retrieve_Base_ITSM_Config')?['body/value'])?['msdyn_value'], '{}'))?['ServiceNowPortalBaseURI'])))" + }, + "host": { + "apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps", + "operationId": "PerformUnboundAction", + "connectionName": "shared_commondataserviceforapps" + } + }, + "runAfter": { + "Flatten_Reference_Fields": [ + "Succeeded" + ] + } + }, + "Set_success_response": { + "type": "SetVariable", + "inputs": { + "name": "ResponseBody", + "value": { + "ServiceNowData": "@outputs('Map_output_fields')?['body/ServiceNowMappedOutput']", + "ErrorMessage": "", + "StatusCode": 200, + "IsSuccessful": "@true", + "ServiceNowSessionID": "", + "ServiceNowTransactionID": "" + } + }, + "runAfter": { + "Map_output_fields": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": { + "Set_failure_response": { + "type": "SetVariable", + "inputs": { + "name": "ResponseBody", + "value": { + "ServiceNowData": "", + "ErrorMessage": "No service catalog request found.", + "StatusCode": 404, + "IsSuccessful": "@false", + "ServiceNowSessionID": "", + "ServiceNowTransactionID": "" + } + }, + "runAfter": {} + } + } + }, + "runAfter": { + "ServiceNow_Get_Request": [ + "Succeeded", + "Failed" + ] + } + }, + "Respond_to_Copilot": { + "type": "Response", + "kind": "Skills", + "inputs": { + "schema": { + "type": "object", + "properties": { + "servicenowdata": { + "title": "ServiceNowData", + "x-ms-dynamically-added": true, + "type": "string", + "description": "Data returned from ServiceNow." + }, + "issuccessful": { + "title": "IsSuccessful", + "x-ms-dynamically-added": true, + "type": "boolean", + "description": "Boolean to indicate whether the flow succeeded or not." + }, + "errormessage": { + "title": "ErrorMessage", + "x-ms-dynamically-added": true, + "type": "string", + "description": "Error message in case of a failure." + }, + "statuscode": { + "title": "StatusCode", + "x-ms-dynamically-added": true, + "type": "number", + "description": "Integer value to indicate the request status code." + }, + "servicenowsessionid": { + "title": "ServiceNowSessionID", + "description": "The glide_session_store cookie.", + "type": "string", + "x-ms-content-hint": "TEXT", + "x-ms-dynamically-added": true + }, + "servicenowtransactionid": { + "title": "ServiceNowTransactionID", + "description": "The X-Transaction-ID header.", + "type": "string", + "x-ms-content-hint": "TEXT", + "x-ms-dynamically-added": true + } + }, + "additionalProperties": {} + }, + "statusCode": 200, + "body": { + "servicenowdata": "@{variables('ResponseBody')?['ServiceNowData']}", + "issuccessful": "@variables('ResponseBody')?['IsSuccessful']", + "errormessage": "@{variables('ResponseBody')?['ErrorMessage']}", + "statuscode": "@variables('ResponseBody')?['StatusCode']", + "servicenowsessionid": "@{variables('ResponseBody')?['ServiceNowSessionID']}", + "servicenowtransactionid": "@{variables('ResponseBody')?['ServiceNowTransactionID']}" + } + }, + "runAfter": { + "Condition_-_ServiceNow_call_succeeded": [ + "Succeeded" + ] + } + }, + "Retrieve_ServiceNow_ITSM_Portal_Base_URI": { + "type": "OpenApiConnection", + "inputs": { + "parameters": { + "entityName": "environmentvariabledefinitions", + "$select": "schemaname,defaultvalue", + "$filter": "schemaname eq 'ServiceNowITSMPortalBaseURI'", + "$expand": "environmentvariabledefinition_environmentvariablevalue($select=value)", + "$top": 1 + }, + "host": { + "apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps", + "operationId": "ListRecords", + "connectionName": "shared_commondataserviceforapps" + } + }, + "runAfter": { + "Retrieve_Base_ITSM_Config": [ + "Succeeded" + ] + } + } + } + } + } +} diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/workflows/get-request-item/metadata.yml b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/workflows/get-request-item/metadata.yml new file mode 100644 index 00000000..5fc2d920 --- /dev/null +++ b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/workflows/get-request-item/metadata.yml @@ -0,0 +1,16 @@ +# Replace {FLOW_GUID} with a newly generated GUID (e.g. from: python -c "import uuid; print(uuid.uuid4())") +# Replace {SERVICENOW_CONNREF} with your agent's ServiceNow connection reference logical name +# Replace {DATAVERSE_CONNREF} with your agent's Dataverse connection reference logical name +# Both connrefs can be found in workspace/agents/{slug}/connectionreferences.mcs.yml +jsonFileName: workflow/workflow.json +workflowId: "{FLOW_GUID}" +name: ESS IT ServiceNow ITSM Get Request Item +type: 1 +description: "Retrieves details for a ServiceNow request item (sc_req_item / RITM) by number." +subprocess: false +category: 5 +mode: 0 +scope: 4 +stateCode: 1 +statusCode: 2 +isTransacted: true diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/workflows/get-request-item/workflow.json b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/workflows/get-request-item/workflow.json new file mode 100644 index 00000000..782919ad --- /dev/null +++ b/copilot-agent-samples/ESS/servicenow-itsm-get-request-details/workflows/get-request-item/workflow.json @@ -0,0 +1,307 @@ +{ + "schemaVersion": "1.0.0.0", + "properties": { + "connectionReferences": { + "shared_service-now": { + "api": { + "name": "shared_service-now" + }, + "connection": { + "connectionReferenceLogicalName": "{SERVICENOW_CONNREF}" + }, + "runtimeSource": "invoker" + }, + "shared_commondataserviceforapps": { + "api": { + "name": "shared_commondataserviceforapps" + }, + "connection": { + "connectionReferenceLogicalName": "{DATAVERSE_CONNREF}" + }, + "runtimeSource": "embedded" + } + }, + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "$authentication": { + "defaultValue": {}, + "type": "SecureObject" + }, + "$connections": { + "defaultValue": {}, + "type": "Object" + } + }, + "triggers": { + "manual": { + "type": "Request", + "kind": "Skills", + "inputs": { + "schema": { + "type": "object", + "properties": { + "text": { + "description": "RITM number (e.g. RITM0010002) of the request item to retrieve.", + "title": "Identifier", + "type": "string", + "x-ms-content-hint": "TEXT", + "x-ms-dynamically-added": true + } + }, + "required": [ + "text" + ] + } + } + } + }, + "actions": { + "Initialize_ResponseBody": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "ResponseBody", + "type": "object" + } + ] + }, + "runAfter": {} + }, + "Retrieve_Base_ITSM_Config": { + "type": "OpenApiConnection", + "inputs": { + "parameters": { + "entityName": "msdyn_employeeselfservicetemplateconfigs", + "$select": "msdyn_value", + "$filter": "msdyn_uniquename eq 'msdyn_ServiceNowITSM'", + "$top": 1 + }, + "host": { + "apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps", + "operationId": "ListRecords", + "connectionName": "shared_commondataserviceforapps" + } + }, + "runAfter": { + "Initialize_ResponseBody": [ + "Succeeded" + ] + } + }, + "Find_Request_Item": { + "type": "OpenApiConnection", + "inputs": { + "parameters": { + "tableType": "sc_req_item", + "sysparm_query": "@concat('number=', triggerBody()?['text'])", + "sysparm_fields": "number,short_description,state,cat_item,request,assigned_to,sys_updated_on,sys_id,quantity,description", + "sysparm_limit": 1, + "sysparm_display_value": true, + "sysparm_exclude_reference_link": false + }, + "host": { + "apiId": "/providers/Microsoft.PowerApps/apis/shared_service-now", + "operationId": "GetRecords", + "connectionName": "shared_service-now" + } + }, + "runAfter": { + "Retrieve_ServiceNow_ITSM_Portal_Base_URI": [ + "Succeeded" + ] + }, + "limit": { + "timeout": "PT1M" + } + }, + "Condition_Record_Found": { + "type": "If", + "expression": { + "and": [ + { + "greater": [ + "@length(coalesce(body('Find_Request_Item')?['result'], json('[]')))", + 0 + ] + } + ] + }, + "runAfter": { + "Find_Request_Item": [ + "Succeeded", + "Failed" + ] + }, + "actions": { + "Store_First_Result": { + "type": "Compose", + "inputs": "@first(body('Find_Request_Item')?['result'])", + "runAfter": {} + }, + "Flatten_Reference_Fields": { + "type": "Compose", + "inputs": { + "number": "@outputs('Store_First_Result')?['number']", + "short_description": "@outputs('Store_First_Result')?['short_description']", + "description": "@outputs('Store_First_Result')?['description']", + "state": "@outputs('Store_First_Result')?['state']", + "cat_item": "@{if(startsWith(string(outputs('Store_First_Result')?['cat_item']), '{'), json(string(outputs('Store_First_Result')?['cat_item']))?['display_value'], string(outputs('Store_First_Result')?['cat_item']))}", + "request": "@{if(startsWith(string(outputs('Store_First_Result')?['request']), '{'), json(string(outputs('Store_First_Result')?['request']))?['display_value'], string(outputs('Store_First_Result')?['request']))}", + "assigned_to": "@{if(startsWith(string(outputs('Store_First_Result')?['assigned_to']), '{'), json(string(outputs('Store_First_Result')?['assigned_to']))?['display_value'], string(outputs('Store_First_Result')?['assigned_to']))}", + "quantity": "@outputs('Store_First_Result')?['quantity']", + "sys_updated_on": "@outputs('Store_First_Result')?['sys_updated_on']", + "sys_id": "@outputs('Store_First_Result')?['sys_id']" + }, + "runAfter": { + "Store_First_Result": [ + "Succeeded" + ] + } + }, + "Map_output_fields": { + "type": "OpenApiConnection", + "inputs": { + "parameters": { + "actionName": "msdyn_ServiceNowOutputFieldMapperPlugin", + "item/ServiceNowTableData": "@outputs('Flatten_Reference_Fields')", + "item/OutputFieldMapping": "[{\"FieldName\":\"number\",\"OutputName\":\"RequestItemNumber\"},{\"FieldName\":\"short_description\",\"OutputName\":\"ShortDescription\"},{\"FieldName\":\"description\",\"OutputName\":\"Description\"},{\"FieldName\":\"state\",\"OutputName\":\"State\"},{\"FieldName\":\"cat_item\",\"OutputName\":\"CatalogItem\"},{\"FieldName\":\"request\",\"OutputName\":\"ParentRequest\"},{\"FieldName\":\"assigned_to\",\"OutputName\":\"AssignedTo\"},{\"FieldName\":\"quantity\",\"OutputName\":\"Quantity\"},{\"FieldName\":\"sys_updated_on\",\"OutputName\":\"LastUpdated\"},{\"FieldName\":\"sys_id\",\"OutputName\":\"SysId\"}]", + "item/ServiceNowITSMBaseConfiguration": "@setProperty(json(coalesce(first(outputs('Retrieve_Base_ITSM_Config')?['body/value'])?['msdyn_value'], '{}')), 'ServiceNowPortalBaseURI', if(not(empty(coalesce(first(first(outputs('Retrieve_ServiceNow_ITSM_Portal_Base_URI')?['body/value'])?['environmentvariabledefinition_environmentvariablevalue'])?['value'], ''))), first(first(outputs('Retrieve_ServiceNow_ITSM_Portal_Base_URI')?['body/value'])?['environmentvariabledefinition_environmentvariablevalue'])?['value'], if(not(empty(coalesce(first(outputs('Retrieve_ServiceNow_ITSM_Portal_Base_URI')?['body/value'])?['defaultvalue'], ''))), first(outputs('Retrieve_ServiceNow_ITSM_Portal_Base_URI')?['body/value'])?['defaultvalue'], json(coalesce(first(outputs('Retrieve_Base_ITSM_Config')?['body/value'])?['msdyn_value'], '{}'))?['ServiceNowPortalBaseURI'])))" + }, + "host": { + "apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps", + "operationId": "PerformUnboundAction", + "connectionName": "shared_commondataserviceforapps" + } + }, + "runAfter": { + "Flatten_Reference_Fields": [ + "Succeeded" + ] + } + }, + "Set_success_response": { + "type": "SetVariable", + "inputs": { + "name": "ResponseBody", + "value": { + "ServiceNowData": "@outputs('Map_output_fields')?['body/ServiceNowMappedOutput']", + "ErrorMessage": "", + "IsSuccessful": "@true", + "ServiceNowSessionID": "", + "ServiceNowTransactionID": "" + } + }, + "runAfter": { + "Map_output_fields": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": { + "Set_not_found": { + "type": "SetVariable", + "inputs": { + "name": "ResponseBody", + "value": { + "ServiceNowData": "", + "ErrorMessage": "No request item found.", + "IsSuccessful": "@false", + "ServiceNowSessionID": "", + "ServiceNowTransactionID": "" + } + }, + "runAfter": {} + } + } + } + }, + "Respond_to_Copilot": { + "type": "Response", + "kind": "Skills", + "inputs": { + "schema": { + "type": "object", + "properties": { + "servicenowdata": { + "title": "ServiceNowData", + "x-ms-dynamically-added": true, + "type": "string" + }, + "issuccessful": { + "title": "IsSuccessful", + "x-ms-dynamically-added": true, + "type": "boolean" + }, + "errormessage": { + "title": "ErrorMessage", + "x-ms-dynamically-added": true, + "type": "string" + }, + "statuscode": { + "title": "StatusCode", + "x-ms-dynamically-added": true, + "type": "number" + }, + "servicenowsessionid": { + "title": "ServiceNowSessionID", + "type": "string", + "x-ms-content-hint": "TEXT", + "x-ms-dynamically-added": true + }, + "servicenowtransactionid": { + "title": "ServiceNowTransactionID", + "type": "string", + "x-ms-content-hint": "TEXT", + "x-ms-dynamically-added": true + } + }, + "additionalProperties": {} + }, + "statusCode": 200, + "body": { + "servicenowdata": "@{variables('ResponseBody')?['ServiceNowData']}", + "issuccessful": "@variables('ResponseBody')?['IsSuccessful']", + "errormessage": "@{variables('ResponseBody')?['ErrorMessage']}", + "statuscode": 200, + "servicenowsessionid": "", + "servicenowtransactionid": "" + } + }, + "runAfter": { + "Condition_Record_Found": [ + "Succeeded", + "Failed" + ] + } + }, + "Retrieve_ServiceNow_ITSM_Portal_Base_URI": { + "type": "OpenApiConnection", + "inputs": { + "parameters": { + "entityName": "environmentvariabledefinitions", + "$select": "schemaname,defaultvalue", + "$filter": "schemaname eq 'ServiceNowITSMPortalBaseURI'", + "$expand": "environmentvariabledefinition_environmentvariablevalue($select=value)", + "$top": 1 + }, + "host": { + "apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps", + "operationId": "ListRecords", + "connectionName": "shared_commondataserviceforapps" + } + }, + "runAfter": { + "Retrieve_Base_ITSM_Config": [ + "Succeeded" + ] + } + } + } + } + } +} diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/README.md b/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/README.md new file mode 100644 index 00000000..2175195a --- /dev/null +++ b/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/README.md @@ -0,0 +1,239 @@ +# ESS ServiceNow ITSM โ€“ Get User Requests + +## ๐Ÿ“Œ Overview + +Extends the Microsoft Employee Self-Service (ESS) agent with the ability to list a user's **service catalog requests** (REQ numbers) from ServiceNow. The built-in ESS ITSM orchestrator only handles incident lists (INC). This sample adds a standalone flow that queries the `sc_request` table directly. + +**Example conversations:** + +> User: "Show me my open requests" +> Agent: *[Numbered list of active REQ items with links]* + +> User: "What requests have I submitted?" +> Agent: *[Numbered list of all REQ items, most recently updated first]* + +--- + +## ๐Ÿ“ Files + +| File | Purpose | +|------|---------| +| `topics/ess-it-servicenow-itsm-get-user-requests.mcs.yml` | User-facing topic โ€” trigger phrases, state extraction, cache check, list rendering | +| `topics/ess-it-servicenow-itsm-system-get-user-requests.mcs.yml` | System topic โ€” routes to the standalone flow and populates the 15-min cache | +| `workflow/workflow.json` | Raw flow definition โ€” queries `sc_request` filtered by user email + active state; used by **Option A** | +| `workflow/metadata.yml` | Flow metadata for the ESS Maker Kit push script | +| `solution/ESSServiceNowITSMGetUserRequests_1_0_0_0.zip` | Unmanaged Power Platform solution containing the flow and its ServiceNow connection reference โ€” **Option B** | + +> **โš ๏ธ Topic schema names depend on how you install.** The user-facing topic reaches the system +> topic by schema name, e.g. +> `dialog: msdyn_copilotforemployeeselfserviceit.topic.ess-it-servicenow-itsm-system-get-user-requests` +> (two occurrences). How that schema name gets assigned differs by install path: +> +> | Install path | Schema name derived from | Result | +> |---|---|---| +> | **Option A** (`push.py`) | the **filename** | `โ€ฆtopic.ess-it-servicenow-itsm-system-get-user-requests` | +> | **Option B** (Copilot Studio portal) | the **display name**, spaces removed | `โ€ฆtopic.ESSITServiceNowITSMSystemGetUserRequests` | +> +> The references in this sample are written for **Option A** and resolve as-is. If you install +> via the portal, update both `dialog:` lines in +> `topics/ess-it-servicenow-itsm-get-user-requests.mcs.yml` to the PascalCase form. A mismatch +> fails at publish with `Dialog with id 'โ€ฆ' not found`, followed by a second, misleading +> `IncorrectTypeAssignment` error (`Assigned: Unspecified, expected: String`) โ€” that one is a +> symptom of the unresolved reference, not a separate problem, and clears with it. +> +> Also note the `msdyn_copilotforemployeeselfserviceit` prefix is the **agent's** schema name. +> These topics target the ESS **IT** agent; retarget the prefix if your agent differs. + +--- + +## โœ… Prerequisites + +- Microsoft Employee Self-Service agent (deployed) +- ServiceNow ITSM extension pack installed in Copilot Studio +- An active ServiceNow connection in Power Platform configured for on-behalf-of (invoker) auth โ€” see [Configure OBO](#-configure-obo-required) +- The **`ServiceNowITSMPortalBaseURI`** environment variable populated โ€” see [Set the portal base URI](#-set-the-portal-base-uri-required-for-links) +- [ESS Maker Kit](https://github.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/tree/main/solutions/ess-maker-skills) (for automated deployment via `push.py`) + +--- + +## ๐Ÿš€ Setup + +> **โš ๏ธ OBO (on-behalf-of) is required for this flow.** The ServiceNow connection uses +> `"runtimeSource": "invoker"`, so the flow calls ServiceNow **as the signed-in employee**, +> not as the maker. This keeps ServiceNow's own row-level ACLs in force โ€” each user only sees +> requests they're entitled to. See [Configure OBO](#-configure-obo-required) below. + +> **๐Ÿ”— Also required:** set the `ServiceNowITSMPortalBaseURI` environment variable. +> It ships empty, and the "Open in ServiceNow" links render without a host until you set it. +> See [Set the portal base URI](#-set-the-portal-base-uri-required-for-links) below. + +### Option A โ€” [ESS Maker Kit](https://github.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/tree/main/solutions/ess-maker-skills) (recommended) + +1. Run `/setup` in your ESS Maker Kit workspace to connect to the environment. +2. Copy `topics/` โ†’ `workspace/agents/{slug}/topics/`. +3. Copy `workflow/` โ†’ `workspace/agents/{slug}/workflows/ess-it-servicenow-itsm-get-user-requests-{NEW-GUID}/`. +4. Replace the placeholders: + - `{FLOW_GUID}` โ€” in the system topic **and** `workflow/metadata.yml` (use the GUID from step 3). + - `{SERVICENOW_CONNREF}` / `{DATAVERSE_CONNREF}` โ€” in `workflow.json`, from `workspace/agents/{slug}/connectionreferences.mcs.yml`. +5. `python scripts/push.py` +6. **Configure OBO** โ€” see below. Do this *before* turning the flow on. +7. Turn the flow **on** in [Power Automate](https://make.powerautomate.com) (API activation is blocked for ServiceNow flows). +8. `python scripts/push.py --repair "Get User Requests"` to wire topic โ†’ flow. +9. Publish the agent in Copilot Studio. + +### Option B โ€” Import the solution + +The solution ships the flow plus its own ServiceNow connection reference +(`esss_servicenow`), so there is exactly one connection to map at import time. + +#### B1 โ€” Import the solution + +1. Go to [Power Apps](https://make.powerapps.com) and confirm the environment picker (top right) shows your target environment. +2. Select **Solutions** in the left nav, then **Import solution** on the command bar. +3. Select **Browse**, choose `solution/ESSServiceNowITSMGetUserRequests_1_0_0_0.zip`, then **Next**. +4. On the **Connections** step, map **ESS Sample ServiceNow Connection** to your ServiceNow connection. If you don't have one, select **+ New connection**, create it, then return and select **Refresh**. + - You will **not** be asked to map a Dataverse connection. The flow binds Dataverse through `new_sharedcommondataserviceforapps_41c83`, which is already in your environment courtesy of the ServiceNow extension pack. +5. Select **Import** and wait for the success banner. + +#### B2 โ€” Configure OBO (do this before testing) + +The flow calls ServiceNow **as the signed-in employee**, which requires parameter sharing on the +ServiceNow connection. This is configured in **Copilot Studio**, not Power Automate โ€” follow +[Configure OBO](#-configure-obo-required) below, then return here. + +#### B3 โ€” Collect the flow ID + +1. In [Power Automate](https://make.powerautomate.com), select **Solutions**, then open **ESS ServiceNow ITSM - Get User Requests**. +2. Confirm **ESS IT ServiceNow ITSM Get User Requests** shows **Status: On**. If it's **Off**, open it and select **Turn on**. +3. Open the flow and read its ID from the browser address bar โ€” it's the GUID immediately after `/flows/`: + + ```text + https://make.powerautomate.com/environments/{env}/solutions/{sol}/flows/5f08a17c-ed32-42ea-bbe4-29da0e9e98b1/details + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ flow ID โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + ``` + +#### B4 โ€” Create the two topics + +For each file in `topics/`: + +1. In [Copilot Studio](https://copilotstudio.microsoft.com/), open your agent and select **Topics** โ†’ **+ Add a topic** โ†’ **From blank**. +2. Open the **โ‹ฎ** menu and select **Open code editor**. +3. Paste the entire contents of the `.mcs.yml` file, replacing whatever is already there. +4. Set the topic name **exactly** as below. The name matters: the portal derives the topic's schema name from it, and the user-facing topic locates the system topic by schema name. + + | File | Topic name to use | + |------|-------------------| + | `ess-it-servicenow-itsm-system-get-user-requests.mcs.yml` | `ESS IT ServiceNow ITSM System Get User Requests` | + | `ess-it-servicenow-itsm-get-user-requests.mcs.yml` | `ESS IT ServiceNow ITSM Get User Requests` | + +5. In the **system** topic, replace `{FLOW_GUID}` with the flow ID from B3. +6. In the **user-facing** topic, replace **both** `dialog:` values (they appear twice โ€” once in the all-requests branch, once in the filtered branch) with the PascalCase schema name the portal generated: + + ```yaml + dialog: msdyn_copilotforemployeeselfserviceit.topic.ESSITServiceNowITSMSystemGetUserRequests + ``` + + The shipped values are kebab-case (correct for Option A only). Leaving them produces + `Dialog with id 'โ€ฆ' not found` at publish โ€” see the note under [Files](#-files). +7. **Save** each topic. + +#### B5 โ€” Publish + +Select **Publish** in Copilot Studio. If publish reports `Dialog with id โ€ฆ not found`, revisit +step B4.6; if it reports `IncorrectTypeAssignment` alongside it, that's the same fault, not a +second one. + +> The solution declares the ServiceNow connection as `"runtimeSource": "invoker"`, so an imported +> flow is wired for OBO from the start. You still need the connection-side step in B2. + +### ๐Ÿ”— Set the portal base URI (required for links) + +The flow in this sample resolve the "Open in ServiceNow" link base the same way the +out-of-the-box ESS orchestrator does: + +1. the **`ServiceNowITSMPortalBaseURI`** environment variable's **current value**, then +2. that variable's **default value**, then +3. the `ServiceNowPortalBaseURI` property of the `msdyn_ServiceNowITSM` template + configuration record. + +**This environment variable ships empty**, and so does the template configuration +property. If you don't set one of them, the adaptive cards still render correctly, +but every "Open in ServiceNow" link points at `?id=ticket&table=...` with no host +and won't resolve. + +1. Go to [make.powerapps.com](https://make.powerapps.com) โ†’ your environment โ†’ + **Solutions** โ†’ **Default Solution** โ†’ **Environment variables**. +2. Open **ServiceNowITSMPortalBaseURI**. +3. Set the **Current value** to your ServiceNow portal base URL โ€” no trailing + slash, no query string: + + ```text + https://.service-now.com/esc + ``` + + Use `/esc` for **Employee Center** or `/sp` for the classic **Service Portal**, + whichever your employees use. To confirm which is default in your instance, go + to **Service Portal โ†’ Portals** in ServiceNow and check the **URL suffix** of + the portal flagged as default. +4. **Save**. + +The topics append `?id=ticket&table=sc_request&sys_id=...` to this value, so a +finished link looks like: + +```text +https://.service-now.com/esc?id=ticket&table=sc_request&sys_id=127a3aef8356cf1028e6cc65eeaad3d4 +``` + +> This variable is shared with the out-of-the-box ServiceNow ITSM topics โ€” +> setting it fixes their links too. + +--- + +### ๐Ÿ” Configure OBO (required) + +The flow must call ServiceNow as the signed-in employee so ServiceNow enforces per-user access +itself. Enable parameter sharing on the ServiceNow connection backing this flow: + +1. In [Copilot Studio](https://copilotstudio.microsoft.com/), select **Agents** and open your agent. +2. Select **Settings**, then **Connection Settings**. +3. Find the **ServiceNow** connection row. Under **Manage**, select **See details**. +4. Open the **Connection parameters** tab. +5. Turn on **Allow permission to share parameters**. +6. Select the checkboxes for the parameters you want the user to share. + +Each employee is prompted to grant permission the first time the agent uses the connection on +their behalf. Full reference: +[Share connection parameters for On-Behalf-Of (OBO) authentication](https://learn.microsoft.com/en-us/microsoft-copilot-studio/authoring-connections#share-connection-parameters-for-on-behalf-of-obo-authentication). + +On the ServiceNow side, ensure the OAuth application registry entry allows your Entra ID users +and that they have read access to `sc_request`. + +Confirm it worked: `workflow.json` should still show `"runtimeSource": "invoker"` on the +ServiceNow connection reference after deployment. If it flips to `"embedded"`, the flow is +running as the maker โ€” every user would see the maker's data, so reconfigure before shipping. + +The `requested_for.email` filter stays in the query regardless. With OBO it's defence-in-depth +rather than the sole isolation boundary. Note that the 15-minute cache is per-user under OBO. + +> **Note:** configuring OBO through the Copilot Studio UI rewrites the flow's +> `connectionReferences` block (the key typically becomes `shared_service-now-1`). If you +> re-export or re-push the flow later, re-check that block so you don't overwrite the live +> OBO binding with a stale one. + +--- + +## โš ๏ธ Known Limitations + +- **Activation via API fails** โ€” same as other ServiceNow standalone flows in this collection. Manual turn-on in Power Automate is required. +- **Per-user cache** โ€” because the flow runs as the invoker, the 15-minute cache holds each employee's own results. It is not shared across users. +- **Global cache variables** โ€” the topic uses `Global.ESS_ServiceNow_AllRequests` and `Global.ESS_ServiceNow_NextAllRequestsRefresh`. These are agent-scoped global variables โ€” ensure they don't conflict with other topics in your agent. +- **Reference fields** โ€” `requested_for` and `assigned_to` come back from `GetRecords` as `{display_value, link}` objects when populated (or an empty string when not set). The flow's `Flatten_Request_Records` **Select** step normalizes these to plain display-value strings across every row before mapping โ€” if you add more reference fields to `sysparm_fields`, apply the same flatten pattern. +- **Result key** โ€” the ServiceNow connector returns rows under `result` (ServiceNow Table API v2), *not* OData's `value`. Feeding `body(...)?['value']` to the output mapper yields `null` and fails with `A null value was found for the property named 'ServiceNowTableData'`. Keep the `?['result']` references intact if you customise this flow. + +--- + +## Author + +| Author | Original Publish Date | Latest Publish Date | +| --- | --- | --- | +| Dean Cron | 2026-07-30 | 2026-08-03 | diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/solution/ESSServiceNowITSMGetUserRequests_1_0_0_0.zip b/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/solution/ESSServiceNowITSMGetUserRequests_1_0_0_0.zip new file mode 100644 index 00000000..8fa56f11 Binary files /dev/null and b/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/solution/ESSServiceNowITSMGetUserRequests_1_0_0_0.zip differ diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/topics/ess-it-servicenow-itsm-get-user-requests.mcs.yml b/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/topics/ess-it-servicenow-itsm-get-user-requests.mcs.yml new file mode 100644 index 00000000..14186b3f --- /dev/null +++ b/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/topics/ess-it-servicenow-itsm-get-user-requests.mcs.yml @@ -0,0 +1,102 @@ +kind: AdaptiveDialog +inputs: + - kind: AutomaticTaskInput + propertyName: RequestState + description: |- + RequestState indicates the state of the requests the user wants to fetch. For active, open and pending requests, RequestState value will be 'active'. Only for closed/completed requests, RequestState value will be 'inactive'. If not sure of the state or if the user is requesting all requests, RequestState will be 'all'. + Examples: + query - "List of my open requests"; RequestState value - "active" + query - "My pending requests"; RequestState value - "active" + query - "List of my requests"; RequestState - "all" + query - "My completed requests"; RequestState value - "inactive" + query - "My closed requests"; RequestState value - "inactive" + entity: StringPrebuiltEntity + shouldPromptUser: false + +modelDescription: |- + This tool retrieves the list of service catalog requests (REQ numbers) for the user. + + Use this tool when a user asks about their service requests, catalog requests, or REQ items โ€” NOT for incident tickets (INC). Use ServiceNow-ITSM-Get-User-Tickets for incidents. + + Example queries: + * "Show me my service requests" + * "What requests do I have open?" + * "My pending catalog requests" + * "List my completed requests" + * "What did I request recently?" + * "Show my REQ items" + + Use these instructions to respond: + 1. Use the 'ServiceNowData' variable to answer the user's question. + 2. If no requests are found, tell the user. + 3. If requests are found, note they are sorted by most recently updated. + 4. Display them in a numbered list in the format: + `**[{RequestNumber}]({ServiceNowPortalBaseURI}?id=ticket&table=sc_request&sys_id={SysId})**: {ShortDescription} ({State})`. + 5. Let the user know you can provide more details on any specific request. +beginDialog: + kind: OnRecognizedIntent + id: main + intent: {} + actions: + - kind: ConditionGroup + id: conditionGroup_checkCache + conditions: + - id: conditionItem_useCache + condition: =!IsBlank(Global.ESS_ServiceNow_AllRequests) && !IsBlank(Global.ESS_ServiceNow_NextAllRequestsRefresh) && Global.ESS_ServiceNow_NextAllRequestsRefresh > Now() && (Topic.RequestState = "all" || IsBlank(Topic.RequestState)) + displayName: Requests list is cached? + actions: + - kind: SetVariable + id: useCachedRequests + variable: Topic.ServiceNowData + value: =Global.ESS_ServiceNow_AllRequests + + - id: conditionItem_allRequests + condition: =Topic.RequestState = "all" || IsBlank(Topic.RequestState) + displayName: List all requests + actions: + - kind: BeginDialog + id: callSystemGetAllRequests + input: + binding: + IsActive: =Blank() + UserIdentifier: =Global.ESS_UserContext_UPN + + dialog: msdyn_copilotforemployeeselfserviceit.topic.ess-it-servicenow-itsm-system-get-user-requests + output: + binding: + ServiceNowData: Topic.ServiceNowData + + elseActions: + - kind: SetVariable + id: setIsActive + variable: Topic.IsActive + value: =If(Topic.RequestState = "active", true, false) + + - kind: BeginDialog + id: callSystemGetFilteredRequests + input: + binding: + IsActive: =Topic.IsActive + UserIdentifier: =Global.ESS_UserContext_UPN + + dialog: msdyn_copilotforemployeeselfserviceit.topic.ess-it-servicenow-itsm-system-get-user-requests + output: + binding: + ServiceNowData: Topic.ServiceNowData + +inputType: + properties: + RequestState: + displayName: RequestState + description: |- + RequestState indicates the state of the requests the user wants to fetch. For active, open and pending requests, RequestState value will be 'active'. Only for closed/completed requests, RequestState value will be 'inactive'. If not sure of the state or if the user is requesting all requests, RequestState will be 'all'. + type: String + examples: + +outputType: + properties: + ServiceNowData: + displayName: ServiceNowData + description: List of service catalog requests returned from ServiceNow. + type: String + examples: diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/topics/ess-it-servicenow-itsm-system-get-user-requests.mcs.yml b/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/topics/ess-it-servicenow-itsm-system-get-user-requests.mcs.yml new file mode 100644 index 00000000..71aca5f4 --- /dev/null +++ b/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/topics/ess-it-servicenow-itsm-system-get-user-requests.mcs.yml @@ -0,0 +1,66 @@ +kind: AdaptiveDialog +inputs: + - kind: ManualTaskInput + propertyName: IsActive + value: =true + + - kind: ManualTaskInput + propertyName: UserIdentifier + value: "=\"\"" + +beginDialog: + kind: OnRedirect + id: main + actions: + - kind: InvokeFlowAction + id: callGetUserRequestsFlow + input: + binding: + text: =Topic.UserIdentifier + text_1: =If(IsBlank(Topic.IsActive), "", Text(Topic.IsActive)) + + output: + binding: + errormessage: Topic.ErrorMessage + issuccessful: Topic.IsSuccessful + servicenowdata: Topic.ServiceNowData + servicenowsessionid: Topic.ServiceNowSessionID + servicenowtransactionid: Topic.ServiceNowTransactionID + statuscode: Topic.StatusCode + + flowId: {FLOW_GUID} + + - kind: ConditionGroup + id: conditionGroup_cacheResult + conditions: + - id: conditionItem_cacheAll + condition: =!IsBlank(Topic.ServiceNowData) && IsBlank(Topic.IsActive) + displayName: Cache all-requests response? + actions: + - kind: SetVariable + id: cacheAllRequests + variable: Global.ESS_ServiceNow_AllRequests + value: =Topic.ServiceNowData + + - kind: SetVariable + id: setCacheExpiry + variable: Global.ESS_ServiceNow_NextAllRequestsRefresh + value: =DateAdd(Now(), 15, TimeUnit.Minutes) + +inputType: + properties: + IsActive: + displayName: IsActive + type: Boolean + + UserIdentifier: + displayName: UserIdentifier + type: String + +outputType: + properties: + ServiceNowData: + displayName: ServiceNowData + description: List of service catalog requests returned from ServiceNow. + type: String + diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/workflow/metadata.yml b/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/workflow/metadata.yml new file mode 100644 index 00000000..842baa02 --- /dev/null +++ b/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/workflow/metadata.yml @@ -0,0 +1,13 @@ +jsonFileName: workflows/workflow/workflow.json +workflowId: {FLOW_GUID} +name: ESS IT ServiceNow ITSM Get User Requests +type: 1 +description: "Retrieves a list of service catalog requests (sc_request) for the current user." +subprocess: false +category: 5 +mode: 0 +scope: 4 +stateCode: 1 +statusCode: 2 +isTransacted: true + diff --git a/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/workflow/workflow.json b/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/workflow/workflow.json new file mode 100644 index 00000000..f2be2714 --- /dev/null +++ b/copilot-agent-samples/ESS/servicenow-itsm-get-user-requests/workflow/workflow.json @@ -0,0 +1,323 @@ +{ + "schemaVersion": "1.0.0.0", + "properties": { + "connectionReferences": { + "shared_service-now": { + "api": { + "name": "shared_service-now" + }, + "connection": { + "connectionReferenceLogicalName": "{SERVICENOW_CONNREF}" + }, + "runtimeSource": "invoker" + }, + "shared_commondataserviceforapps": { + "api": { + "name": "shared_commondataserviceforapps" + }, + "connection": { + "connectionReferenceLogicalName": "{DATAVERSE_CONNREF}" + }, + "runtimeSource": "embedded" + } + }, + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "$authentication": { + "defaultValue": {}, + "type": "SecureObject" + }, + "$connections": { + "defaultValue": {}, + "type": "Object" + } + }, + "triggers": { + "manual": { + "type": "Request", + "kind": "Skills", + "inputs": { + "schema": { + "type": "object", + "properties": { + "text": { + "description": "The email or UPN of the current user.", + "title": "UserIdentifier", + "type": "string", + "x-ms-content-hint": "TEXT", + "x-ms-dynamically-added": true + }, + "text_1": { + "description": "Filter by active state: 'true' for active, 'false' for inactive, empty for all.", + "title": "IsActive", + "type": "string", + "x-ms-content-hint": "TEXT", + "x-ms-dynamically-added": true + } + }, + "required": [ + "text" + ] + } + } + } + }, + "actions": { + "Initialize_variable_-_ResponseBody": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "ResponseBody", + "type": "object" + } + ] + }, + "runAfter": {} + }, + "Retrieve_Base_ITSM_Config": { + "type": "OpenApiConnection", + "inputs": { + "parameters": { + "entityName": "msdyn_employeeselfservicetemplateconfigs", + "$select": "msdyn_value", + "$filter": "msdyn_uniquename eq 'msdyn_ServiceNowITSM'", + "$top": 1 + }, + "host": { + "apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps", + "operationId": "ListRecords", + "connectionName": "shared_commondataserviceforapps" + } + }, + "runAfter": { + "Initialize_variable_-_ResponseBody": [ + "Succeeded" + ] + } + }, + "Build_Filter_Query": { + "type": "Compose", + "inputs": "@if(equals(triggerBody()?['text_1'], 'true'), concat('requested_for.email=', triggerBody()?['text'], '^active=true^ORDERBYDESCsys_updated_on'), if(equals(triggerBody()?['text_1'], 'false'), concat('requested_for.email=', triggerBody()?['text'], '^active=false^ORDERBYDESCsys_updated_on'), concat('requested_for.email=', triggerBody()?['text'], '^ORDERBYDESCsys_updated_on')))", + "runAfter": { + "Retrieve_ServiceNow_ITSM_Portal_Base_URI": [ + "Succeeded" + ] + } + }, + "ServiceNow_Get_Requests": { + "type": "OpenApiConnection", + "inputs": { + "parameters": { + "tableType": "sc_request", + "sysparm_query": "@outputs('Build_Filter_Query')", + "sysparm_display_value": true, + "sysparm_exclude_reference_link": false, + "sysparm_fields": "number,short_description,request_state,requested_for,assigned_to,sys_updated_on,sys_id,price", + "sysparm_limit": 10, + "sysparm_offset": 0 + }, + "host": { + "apiId": "/providers/Microsoft.PowerApps/apis/shared_service-now", + "operationId": "GetRecords", + "connectionName": "shared_service-now" + } + }, + "runAfter": { + "Build_Filter_Query": [ + "Succeeded" + ] + }, + "limit": { + "timeout": "PT1M" + } + }, + "Condition_-_ServiceNow_call_succeeded": { + "type": "If", + "expression": { + "and": [ + { + "greater": [ + "@length(coalesce(body('ServiceNow_Get_Requests')?['result'], json('[]')))", + 0 + ] + } + ] + }, + "actions": { + "Flatten_Request_Records": { + "type": "Select", + "inputs": { + "from": "@coalesce(body('ServiceNow_Get_Requests')?['result'], json('[]'))", + "select": { + "number": "@item()?['number']", + "short_description": "@item()?['short_description']", + "request_state": "@item()?['request_state']", + "requested_for": "@{if(startsWith(string(item()?['requested_for']), '{'), json(string(item()?['requested_for']))?['display_value'], string(item()?['requested_for']))}", + "assigned_to": "@{if(startsWith(string(item()?['assigned_to']), '{'), json(string(item()?['assigned_to']))?['display_value'], string(item()?['assigned_to']))}", + "sys_updated_on": "@item()?['sys_updated_on']", + "sys_id": "@item()?['sys_id']", + "price": "@item()?['price']" + } + }, + "runAfter": {} + }, + "Map_output_fields": { + "type": "OpenApiConnection", + "inputs": { + "parameters": { + "actionName": "msdyn_ServiceNowOutputFieldMapperPlugin", + "item/ServiceNowTableData": "@body('Flatten_Request_Records')", + "item/OutputFieldMapping": "[{\"FieldName\":\"number\",\"OutputName\":\"RequestNumber\"},{\"FieldName\":\"short_description\",\"OutputName\":\"ShortDescription\"},{\"FieldName\":\"request_state\",\"OutputName\":\"State\"},{\"FieldName\":\"requested_for\",\"OutputName\":\"RequestedFor\"},{\"FieldName\":\"assigned_to\",\"OutputName\":\"AssignedTo\"},{\"FieldName\":\"sys_updated_on\",\"OutputName\":\"LastUpdated\"},{\"FieldName\":\"sys_id\",\"OutputName\":\"SysId\"},{\"FieldName\":\"price\",\"OutputName\":\"Price\"}]", + "item/ServiceNowITSMBaseConfiguration": "@setProperty(json(coalesce(first(outputs('Retrieve_Base_ITSM_Config')?['body/value'])?['msdyn_value'], '{}')), 'ServiceNowPortalBaseURI', if(not(empty(coalesce(first(first(outputs('Retrieve_ServiceNow_ITSM_Portal_Base_URI')?['body/value'])?['environmentvariabledefinition_environmentvariablevalue'])?['value'], ''))), first(first(outputs('Retrieve_ServiceNow_ITSM_Portal_Base_URI')?['body/value'])?['environmentvariabledefinition_environmentvariablevalue'])?['value'], if(not(empty(coalesce(first(outputs('Retrieve_ServiceNow_ITSM_Portal_Base_URI')?['body/value'])?['defaultvalue'], ''))), first(outputs('Retrieve_ServiceNow_ITSM_Portal_Base_URI')?['body/value'])?['defaultvalue'], json(coalesce(first(outputs('Retrieve_Base_ITSM_Config')?['body/value'])?['msdyn_value'], '{}'))?['ServiceNowPortalBaseURI'])))" + }, + "host": { + "apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps", + "operationId": "PerformUnboundAction", + "connectionName": "shared_commondataserviceforapps" + } + }, + "runAfter": { + "Flatten_Request_Records": [ + "Succeeded" + ] + } + }, + "Set_success_response": { + "type": "SetVariable", + "inputs": { + "name": "ResponseBody", + "value": { + "ServiceNowData": "@outputs('Map_output_fields')?['body/ServiceNowMappedOutput']", + "ErrorMessage": "", + "StatusCode": 200, + "IsSuccessful": "@true", + "ServiceNowSessionID": "", + "ServiceNowTransactionID": "" + } + }, + "runAfter": { + "Map_output_fields": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": { + "Set_failure_response": { + "type": "SetVariable", + "inputs": { + "name": "ResponseBody", + "value": { + "ServiceNowData": "", + "ErrorMessage": "No requests found.", + "StatusCode": 404, + "IsSuccessful": "@false", + "ServiceNowSessionID": "", + "ServiceNowTransactionID": "" + } + }, + "runAfter": {} + } + } + }, + "runAfter": { + "ServiceNow_Get_Requests": [ + "Succeeded", + "Failed", + "TimedOut", + "Skipped" + ] + } + }, + "Respond_to_Copilot": { + "type": "Response", + "kind": "Skills", + "inputs": { + "schema": { + "type": "object", + "properties": { + "servicenowdata": { + "title": "ServiceNowData", + "x-ms-dynamically-added": true, + "type": "string", + "description": "List of requests returned from ServiceNow." + }, + "issuccessful": { + "title": "IsSuccessful", + "x-ms-dynamically-added": true, + "type": "boolean", + "description": "Boolean to indicate whether the flow succeeded or not." + }, + "errormessage": { + "title": "ErrorMessage", + "x-ms-dynamically-added": true, + "type": "string", + "description": "Error message in case of a failure." + }, + "statuscode": { + "title": "StatusCode", + "x-ms-dynamically-added": true, + "type": "number", + "description": "HTTP status code." + }, + "servicenowsessionid": { + "title": "ServiceNowSessionID", + "type": "string", + "x-ms-content-hint": "TEXT", + "x-ms-dynamically-added": true + }, + "servicenowtransactionid": { + "title": "ServiceNowTransactionID", + "type": "string", + "x-ms-content-hint": "TEXT", + "x-ms-dynamically-added": true + } + }, + "additionalProperties": {} + }, + "statusCode": 200, + "body": { + "servicenowdata": "@{variables('ResponseBody')?['ServiceNowData']}", + "issuccessful": "@variables('ResponseBody')?['IsSuccessful']", + "errormessage": "@{variables('ResponseBody')?['ErrorMessage']}", + "statuscode": "@variables('ResponseBody')?['StatusCode']", + "servicenowsessionid": "@{variables('ResponseBody')?['ServiceNowSessionID']}", + "servicenowtransactionid": "@{variables('ResponseBody')?['ServiceNowTransactionID']}" + } + }, + "runAfter": { + "Condition_-_ServiceNow_call_succeeded": [ + "Succeeded" + ] + } + }, + "Retrieve_ServiceNow_ITSM_Portal_Base_URI": { + "type": "OpenApiConnection", + "inputs": { + "parameters": { + "entityName": "environmentvariabledefinitions", + "$select": "schemaname,defaultvalue", + "$filter": "schemaname eq 'ServiceNowITSMPortalBaseURI'", + "$expand": "environmentvariabledefinition_environmentvariablevalue($select=value)", + "$top": 1 + }, + "host": { + "apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps", + "operationId": "ListRecords", + "connectionName": "shared_commondataserviceforapps" + } + }, + "runAfter": { + "Retrieve_Base_ITSM_Config": [ + "Succeeded" + ] + } + } + } + } + } +} diff --git a/resource-stats.json b/resource-stats.json index cf2d9c66..ce349318 100644 --- a/resource-stats.json +++ b/resource-stats.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-30T21:49:54.356Z", + "generatedAt": "2026-07-30T14:26:11.041Z", "sources": { "views": "clarity", "upvotes": "github-discussions"