{"id":44658,"date":"2026-05-20T17:43:12","date_gmt":"2026-05-20T12:13:12","guid":{"rendered":"https:\/\/www.inogic.com\/blog\/?p=44658"},"modified":"2026-05-20T17:43:12","modified_gmt":"2026-05-20T12:13:12","slug":"how-to-build-ai-generated-charts-and-pdfs-in-dynamics-365-using-a-pcf-control","status":"publish","type":"post","link":"https:\/\/www.inogic.com\/blog\/2026\/05\/how-to-build-ai-generated-charts-and-pdfs-in-dynamics-365-using-a-pcf-control\/","title":{"rendered":"How to Build AI-Generated Charts and PDFs in Dynamics 365 Using a PCF Control"},"content":{"rendered":"<p><img decoding=\"async\" class=\"alignnone size-full wp-image-44680\" style=\"border: 1px solid #000000; padding: 1px; margin: 1px;\" src=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/How-to-Build-AI-Generated-Charts-and-PDFs-in-Dynamics-365-Using-a-PCF-Control.png\" alt=\"PCF Control\" width=\"2100\" height=\"1200\" srcset=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/How-to-Build-AI-Generated-Charts-and-PDFs-in-Dynamics-365-Using-a-PCF-Control.png 2100w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/How-to-Build-AI-Generated-Charts-and-PDFs-in-Dynamics-365-Using-a-PCF-Control-300x171.png 300w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/How-to-Build-AI-Generated-Charts-and-PDFs-in-Dynamics-365-Using-a-PCF-Control-1024x585.png 1024w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/How-to-Build-AI-Generated-Charts-and-PDFs-in-Dynamics-365-Using-a-PCF-Control-768x439.png 768w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/How-to-Build-AI-Generated-Charts-and-PDFs-in-Dynamics-365-Using-a-PCF-Control-1536x878.png 1536w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/How-to-Build-AI-Generated-Charts-and-PDFs-in-Dynamics-365-Using-a-PCF-Control-2048x1170.png 2048w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/How-to-Build-AI-Generated-Charts-and-PDFs-in-Dynamics-365-Using-a-PCF-Control-660x377.png 660w\" sizes=\"(max-width: 2100px) 100vw, 2100px\" \/>We spend a lot of time working inside Dynamics 365 CRM forms. Accounts, Opportunities, Cases and other commonly used entities. One area that always had room for improvement was how data gets presented within those forms. For example, if a sales manager wants to see the pipeline breakdown for a specific Account, they typically have to switch to a dashboard or open Power BI. If they need a sales proposal for an Opportunity, it has to be created manually outside the record. All of this happens outside the context of the record they are already working in.<\/p>\n<p>This led us to explore better approaches, and that is when we came across the Code Interpreter PCF control. The idea was simple: the form itself could generate that content on the fly. Not a static chart, not a pre-built template, but something that pulls live CRM data, reasons over it, and renders the output right there inside the form. We tested this across two scenarios: an interactive Opportunity Pipeline chart on the Account form, and an AI-generated Sales Proposal PDF on the Opportunity form. This blog walks through both scenarios end to end.<\/p>\n<h3>What Is the Code Interpreter in the Power Platform Context?<\/h3>\n<p>Before moving forward with the build, it is worth clarifying what &#8216;Code Interpreter&#8217; means here, as the term is often used loosely.<\/p>\n<p>In the Power Platform world, Code Interpreter is a capability that can be enabled on prompts created through AI Hub or Copilot Studio. When <strong>enabled<\/strong>, it allows the prompt to dynamically generate and run Python code on the server side <strong>for tasks like<\/strong> data analysis, chart generation, and document creation. Depending on what the Python code produces, the prompt can return either text, such as raw HTML, or files like PDFs or Word documents.<\/p>\n<p>What Microsoft released as a sample is a PCF (Power Apps Component Framework) field control that acts as the front-end consumer of those prompts. It sits on a Model-Driven App form, calls the Dataverse Predict API against the configured prompt, and renders the response, whether that is an interactive HTML chart or a downloadable PDF.<\/p>\n<blockquote><p>Note: The key architectural point here is separation of concerns: the PCF control handles rendering; the prompt handles intelligence. You do not need to modify the You do not need to modify the PCF code to switch between generating charts and documents. The same control adapts dynamically based on what the prompt returns.<\/p><\/blockquote>\n<h3><strong>The Two Scenarios We Built<\/strong><\/h3>\n<p>We wanted to test both output paths the PCF supports, so we set up two concrete use cases:<\/p>\n<p><strong>Scenario 1 Pipeline Chart on the Account Form<\/strong>: When a user opens an Account record with related Opportunities, the control fetches those Opportunities, groups them by sales stage, and renders an interactive bar chart with hover tooltips and stage-level filtering all inline on the form.<\/p>\n<p><strong>Scenario 2 Sales Proposal PDF on the Opportunity Form<\/strong>: When a user opens an Opportunity, the control generates a professional PDF proposal pulling details like estimated revenue, close probability, and the parent Account&#8217;s information. The PDF renders as an embedded preview with a download button, right inside the form.<\/p>\n<p>Both scenarios share the same PCF control code. The only difference is which prompt and therefore which AI Model GUID is wired up on each form.<\/p>\n<h3>Prerequisites<\/h3>\n<p>Before starting, make sure these are in place:<\/p>\n<ul>\n<li><strong>Code Interpreter enabled at the environment level<\/strong>: Power Platform Admin Center \u2192 Copilot \u2192 Settings \u2192 turn on Code generation and execution in Copilot Studio.<\/li>\n<li><strong>AI Builder capacity<\/strong>: The Predict API consumes AI Builder credits. If you run out, you will hit a 403 EntitlementNotAvailable Make sure you have sufficient credits available before proceeding further.<\/li>\n<li><strong>js and PAC CLI<\/strong>: Both needed to build and deploy the PCF control. Run pac auth create to connect your CLI to the target environment.<\/li>\n<li><strong>Dataverse connector access<\/strong>: Your prompts query Dataverse tables (Opportunity, Account), so the connector needs to be added in the prompt configuration.<\/li>\n<\/ul>\n<h3>Scenario 1: Interactive Pipeline Chart on the Account Form<\/h3>\n<p><strong>Step 1: Create the Prompt<\/strong><\/p>\n<p>Navigate to make.powerapps.com \u2192 your environment \u2192 AI Hub \u2192 Prompts \u2192 Create a custom prompt.<\/p>\n<p>Name it something like &#8220;Generate Opportunity Chart&#8221; and toggle Code Interpreter ON in the prompt settings as shown in the image below.<\/p>\n<p><img decoding=\"async\" class=\"alignnone size-full wp-image-44674\" style=\"border: 1px solid #000000; padding: 1px; margin: 1px;\" src=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-1.jpg\" alt=\"How to Build AI-Generated Charts and PDFs in Dynamics 365 Using a PCF Control\" width=\"1879\" height=\"836\" srcset=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-1.jpg 1879w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-1-300x133.jpg 300w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-1-1024x456.jpg 1024w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-1-768x342.jpg 768w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-1-1536x683.jpg 1536w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-1-660x294.jpg 660w\" sizes=\"(max-width: 1879px) 100vw, 1879px\" \/><\/p>\n<p>Add Dataverse connectors for the Opportunity and Account tables, as the prompt needs access to both. To add a Dataverse connector, navigate to the input section and select Dataverse as the knowledge source. From there, select the relevant entity and choose any field from it to establish the connection. In this case, the Account entity is selected with one of its fields, as shown in the images below.<\/p>\n<p><img decoding=\"async\" class=\"alignnone size-full wp-image-44675\" style=\"border: 1px solid #000000; padding: 1px; margin: 1px;\" src=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-2.jpg\" alt=\"How to Build AI-Generated Charts and PDFs in Dynamics 365 Using a PCF Control\" width=\"1877\" height=\"839\" srcset=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-2.jpg 1877w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-2-300x134.jpg 300w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-2-1024x458.jpg 1024w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-2-768x343.jpg 768w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-2-1536x687.jpg 1536w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-2-660x295.jpg 660w\" sizes=\"(max-width: 1877px) 100vw, 1877px\" \/><\/p>\n<p><img decoding=\"async\" class=\"alignnone size-full wp-image-44676\" style=\"border: 1px solid #000000; padding: 1px; margin: 1px;\" src=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-3.jpg\" alt=\"How to Build AI-Generated Charts and PDFs in Dynamics 365 Using a PCF Control\" width=\"1877\" height=\"824\" srcset=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-3.jpg 1877w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-3-300x132.jpg 300w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-3-1024x450.jpg 1024w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-3-768x337.jpg 768w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-3-1536x674.jpg 1536w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-3-660x290.jpg 660w\" sizes=\"(max-width: 1877px) 100vw, 1877px\" \/><\/p>\n<p>Here&#8217;s the prompt we used:<\/p>\n<pre class=\"lang:css gutter:true start:1\">You are a data analyst working with Dynamics 365 CRM data.\r\nYou are given an Account record ID: {{ RecordId }}.\r\nYour task:\r\n1. Use the Dataverse connector to fetch all Opportunity records related\r\nto this Account (filter: _parentaccountid_value eq '{{RecordId}}').\r\n2. Retrieve these columns: name, estimatedvalue, stepname (sales stage),\r\ncloseprobability.\r\n3. Group the Opportunities by their sales stage (stepname).\r\n4. For each stage, calculate: total estimated revenue, count of opportunities,\r\nand average close probability.\r\nYour output:\r\nGenerate a complete, self-contained HTML page with an interactive chart.\r\nRequirements:\r\n- Use inline JavaScript and CSS only. No external CDN or library references.\r\n- Draw a bar chart using HTML5 Canvas or pure SVG showing each sales stage\r\non the X-axis and total estimated revenue on the Y-axis.\r\n- Use a modern color palette (gradient blues\/purples). Add rounded bar corners.\r\n- On hover, show a tooltip with: stage name, total revenue (currency formatted),\r\nnumber of opportunities, and average close probability as percentage.\r\n- Include title: \"Opportunity Pipeline by Stage\" and subtitle with Account name\r\nand total pipeline value.\r\n- Add smooth fade-in animation on page load.\r\n- Make it responsive to fit any container width.<\/pre>\n<p>The Record ID here is coming dynamically from the PCF control. To handle that, go to the input section, select Text and rename that field to RecordId.<\/p>\n<p><strong>Why force inline HTML instead of an image?<\/strong> Because the PCF control injects the response directly into an iframe via srcdoc, all interactivity works out of the box. Hover effects, tooltips, and animations run natively inside it. If the prompt returns a PNG instead, all of that is lost.<\/p>\n<p><strong>Step 2: Get the AI Model GUID<\/strong><\/p>\n<p>Every prompt in AI Hub creates a corresponding record in the msdyn_aimodel Dataverse table. The PCF control needs this GUID to know which prompt to call.<\/p>\n<p>Run this in your browser (replace with your org URL):<\/p>\n<pre class=\"lang:css gutter:true start:1\">https:\/\/[your-org].crm.dynamics.com\/api\/data\/v9.2\/msdyn_aimodels\r\n?$filter=msdyn_name eq 'Generate Opportunity Chart'\r\n&amp;$select=msdyn_aimodelid,msdyn_name\r\n\r\nCopy the msdyn_aimodelid value from the response.<\/pre>\n<p><strong>Step 3: Build and Deploy the PCF Control<\/strong><\/p>\n<p><strong>Option 1 \u2014 Use the Microsoft Sample:<\/strong> Clone the ready-made sample directly from GitHub: <a href=\"https:\/\/github.com\/microsoft\/PowerApps-Samples\/tree\/master\/component-framework\/CodeInterpreterControl\" target=\"_blank\" rel=\"noopener\">https:\/\/github.com\/microsoft\/PowerApps-Samples\/tree\/master\/component-framework\/CodeInterpreterControl<\/a>.<\/p>\n<p>This gives you the complete PCF project with all the rendering logic already built. No setup from scratch needed.<\/p>\n<p><strong>Option 2 \u2014 Create Your Own PCF Project:<\/strong> If you prefer to start fresh and build the control into an existing solution, initialize a new PCF project using:<\/p>\n<p>pac pcf init &#8211;namespace YourNamespace &#8211;name CodeInterpreterControl &#8211;template field npm install<\/p>\n<p>Then wire in the prompt call and rendering logic manually based on the sample as reference. For this blog, we are going with Option 1. Clone the sample and run:<\/p>\n<pre class=\"lang:css gutter:true start:1\">cd CodeInterpreterControl\r\n\r\nnpm install\r\n\r\nnpm run build\r\n\r\npac pcf push --publisher-prefix &lt;your-prefix&gt;<\/pre>\n<p>The control gets pushed directly to your environment. No solution packaging needed for dev\/testing.<\/p>\n<p><strong>Step 4: Configure on the Account Form<\/strong><\/p>\n<ol>\n<li>Create a Single Line Text or Multi Line Text column on the Account table (e.g., chartoutput). This is just an anchor field the PCF doesn&#8217;t actually store data in it.<\/li>\n<li>Open the Account form in the form designer.<\/li>\n<li>Add the column to the form, then go to Properties \u2192 Components \u2192 Add Component \u2192 choose <strong>Code Interpreter Control<\/strong>.<\/li>\n<li>Set <strong>Model ID<\/strong> as a static value \u2192 paste the AI Model GUID.<\/li>\n<li>Set <strong>Record ID<\/strong> as a static value \u2192 enter 00000000-0000-0000-0000-000000000000 (we explain this below).<\/li>\n<li>Save and publish the form. Add the field to a dedicated tab we named it <strong>&#8220;Pipeline Insights&#8221;<\/strong>.<\/li>\n<\/ol>\n<h3><strong>The Result<\/strong><\/h3>\n<p>When a user opens an Account with related Opportunities, the control renders an interactive bar chart inline grouped by sales stage, with hover tooltips and filter checkboxes. No navigation away from the Account record as shown in the image below.<\/p>\n<p><img decoding=\"async\" class=\"alignnone size-full wp-image-44677\" style=\"border: 1px solid #000000; padding: 1px; margin: 1px;\" src=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-4.jpg\" alt=\"How to Build AI-Generated Charts and PDFs in Dynamics 365 Using a PCF Control\" width=\"1797\" height=\"653\" srcset=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-4.jpg 1797w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-4-300x109.jpg 300w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-4-1024x372.jpg 1024w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-4-768x279.jpg 768w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-4-1536x558.jpg 1536w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-4-660x240.jpg 660w\" sizes=\"(max-width: 1797px) 100vw, 1797px\" \/><\/p>\n<h3><strong>Scenario 2: PDF Sales Proposal on the Opportunity Form<\/strong><\/h3>\n<p><strong>Step 1: Create the Prompt<\/strong><\/p>\n<p>Same process new prompt, Code Interpreter ON, Dataverse connectors for Opportunity and Account tables.<\/p>\n<p>Prompt name: &#8220;Generate Sales Proposal&#8221;:<\/p>\n<pre class=\"lang:css gutter:true start:1\">You are a document specialist working with Dynamics 365 CRM data.\r\nYou are given an Opportunity record ID: {{ RecordId }}.\r\nYour task:\r\n1. Fetch the Opportunity record with this ID. Retrieve: name, estimatedvalue,\r\nstepname, closeprobability, description, estimatedclosedate.\r\n2. Fetch the related Account using _parentaccountid_value. Retrieve: name,\r\naddress1_city, address1_stateorprovince, telephone1, emailaddress1.\r\n3. Generate a professional Sales Proposal PDF document with:\r\n- Header: Styled \"PROPOSAL\" banner, current date, proposal number\r\n(format: PROP-YYYY-XXXX).\r\n- Section 1 Executive Summary: Professional paragraph covering the\r\nopportunity, Account name, deal value, and expected close date.\r\n- Section 2 Opportunity Details: Formatted table with name, estimated value,\r\nsales stage, close probability, and expected close date.\r\n- Section 3 Proposed Solution: 3-4 AI-generated recommendations relevant\r\nto the opportunity.\r\n- Section 4 Investment Summary: Pricing table with estimated value,\r\n10% discount row, and final total.\r\n- Section 5 Next Steps: 3 actionable steps based on the current sales stage.\r\n- Footer: \"Valid for 30 days\" notice with page number.\r\nStyling: Navy blue (#1B2A4A) headers, clean layout, professional fonts,\r\nhorizontal dividers between sections.\r\nOutput: Generate this as a PDF file. Return the PDF file as your output.<\/pre>\n<p><strong>Step 2: Get Model GUID and Configure<\/strong><\/p>\n<p>Follow the same steps as Scenario 1. Query msdyn_aimodels to get the GUID for the &#8220;Generate Sales Proposal&#8221; prompt, add the PCF control to the Opportunity form&#8217;s <strong>&#8220;Sales Proposal&#8221;<\/strong> tab, and set the Model ID.<\/p>\n<p><strong>The Result<\/strong><\/p>\n<p>When a sales rep opens an Opportunity, the PDF proposal generates automatically with an embedded preview and a download button right on the form as shown in the image below.<\/p>\n<p><img decoding=\"async\" class=\"alignnone size-full wp-image-44678\" style=\"border: 1px solid #000000; padding: 1px; margin: 1px;\" src=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-5.jpg\" alt=\"How to Build AI-Generated Charts and PDFs in Dynamics 365 Using a PCF Control\" width=\"1608\" height=\"667\" srcset=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-5.jpg 1608w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-5-300x124.jpg 300w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-5-1024x425.jpg 1024w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-5-768x319.jpg 768w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-5-1536x637.jpg 1536w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/05\/PCF-Control-5-660x274.jpg 660w\" sizes=\"(max-width: 1608px) 100vw, 1608px\" \/><\/p>\n<h3>Under the Hood: How the PCF Control Works<\/h3>\n<p>What makes this PCF control particularly interesting is that a single control handles both HTML rendering and file previews using a clean branching pattern. Here&#8217;s how the key files work<\/p>\n<p><strong>The Entry Point index.ts<\/strong><\/p>\n<p>The control&#8217;s lifecycle starts in init(), which calls loadContent():<\/p>\n<pre class=\"lang:css gutter:true start:1\">private async loadContent(): Promise&lt;void&gt; {\r\n\r\nthis.uiManager.setContent(\"\");\r\n\r\nconst validation = this.validationHandler.validateInputs();\r\n\r\nif (!validation.isValid) {\r\n\r\nthis.uiManager.setContent(\r\n\r\nthis.errorHandler.createErrorHtml(validation.errorMessage!)\r\n\r\n);\r\n\r\nreturn;\r\n\r\n}\r\n\r\nthis.uiManager.showSpinner();\r\n\r\ntry {\r\n\r\nconst response = await retrievePromptResponse({\r\n\r\nbaseUrl: validation.baseUrl!,\r\n\r\nmodelId: validation.modelId!,\r\n\r\nrequestInputs: { RecordId: validation.recordId! },\r\n\r\n});\r\n\r\nthis.uiManager.hideSpinner();\r\n\r\nawait this.handleResponse(response);\r\n\r\n} catch (error: unknown) {\r\n\r\nthis.uiManager.hideSpinner();\r\n\r\nthis.handleError(error);\r\n\r\n}\r\n\r\n}<\/pre>\n<p>The handleResponse method is where the branching happens:<\/p>\n<pre class=\"lang:css gutter:true start:1\">private async handleResponse(data: RetrievePromptResponseOutput): Promise&lt;void&gt; {\r\n\r\nif (data.error) {\r\n\r\nconst errorMessage = this.errorHandler.getErrorMessage(\r\n\r\ndata.error.status, data.error.message\r\n\r\n);\r\n\r\nthis.uiManager.setContent(this.errorHandler.createErrorHtml(errorMessage));\r\n\r\nreturn;\r\n\r\n}\r\n\r\n\/\/ File output takes priority\r\n\r\nconst file = data?.responsev2?.predictionOutput?.files?.[0];\r\n\r\nif (file) {\r\n\r\nawait this.handleFileOutput(file);\r\n\r\nreturn;\r\n\r\n}\r\n\r\n\/\/ Otherwise, render as text\/HTML\r\n\r\nthis.handleTextOutput(data?.responsev2?.predictionOutput?.structuredOutput?.text);\r\n\r\n}<\/pre>\n<p>Notice the priority: <strong>files first, then text.<\/strong> If the Code Interpreter generates a PDF (Scenario 2), the files array is populated and the control renders a preview. If it returns HTML (Scenario 1), structuredOutput.text carries the chart markup.<\/p>\n<p><strong>The API Layer: retrievePromptResponse.ts<\/strong><\/p>\n<p>This handles the actual HTTP call to the Dataverse Predict endpoint:<\/p>\n<pre class=\"lang:css gutter:true start:1\">export async function retrievePromptResponse({\r\n\r\nbaseUrl,\r\n\r\nmodelId,\r\n\r\nrequestInputs,\r\n\r\n}: RetrievePromptResponseInput): Promise&lt;RetrievePromptResponseOutput&gt; {\r\n\r\nconst url = `${baseUrl}\/api\/data\/v9.0\/msdyn_aimodels(${modelId})\/Microsoft.Dynamics.CRM.Predict`;\r\n\r\nconst body = {\r\n\r\nversion: \"2.0\",\r\n\r\nrequestv2: {\r\n\r\n\"@odata.type\": \"#Microsoft.Dynamics.CRM.expando\",\r\n\r\n...requestInputs,\r\n\r\n},\r\n\r\n};\r\n\r\nconst response = await fetch(url, {\r\n\r\nmethod: \"POST\",\r\n\r\nheaders: { \"Content-Type\": \"application\/json\" },\r\n\r\nbody: JSON.stringify(body),\r\n\r\n});\r\n\r\nconst data = (await response.json()) as RetrievePromptResponseOutput;\r\n\r\nreturn data;\r\n\r\n}<\/pre>\n<p>The endpoint follows the Dataverse bound action pattern: msdyn_aimodels({modelId})\/Microsoft.Dynamics.CRM.Predict. The requestv2 body uses the expando OData type\u00a0\u00a0 essentially an open dictionary\u00a0\u00a0 so you can pass any key-value pairs your prompt expects. In our case, that&#8217;s RecordId.<\/p>\n<p><strong>File Handling: filePreviewHandler.ts<\/strong><\/p>\n<p>When the prompt returns a file (Scenario 2), this handler takes over:<\/p>\n<pre class=\"lang:css gutter:true start:1\">public async generatePreview(\r\n\r\nbase64Content: string,\r\n\r\nmimeType: string,\r\n\r\nfileName: string\r\n\r\n): Promise&lt;FilePreviewResult&gt; {\r\n\r\nif (mimeType === \"application\/pdf\") {\r\n\r\nreturn this.generatePdfPreview(base64Content, fileName);\r\n\r\n} else if (mimeType.includes(\"wordprocessingml\") || mimeType.includes(\"msword\")) {\r\n\r\nreturn this.generateDocxPreview(base64Content, fileName);\r\n\r\n} else if (mimeType.includes(\"spreadsheetml\") || mimeType.includes(\"ms-excel\")) {\r\n\r\nreturn this.generateXlsxPreview(base64Content, fileName);\r\n\r\n}\r\n\r\n}<\/pre>\n<p>For PDFs it creates a blob URL from the base64 content and loads it in an iframe. For Word documents, mammoth converts .docx content to HTML. For Excel files, SheetJS (xlsx) handles the conversion. The control also generates a download button alongside the preview automatically.<\/p>\n<p><strong>UI Rendering: uiManager.ts<\/strong><\/p>\n<p>Text output (Scenario 1) goes through setContent(), which sets the srcdoc attribute on an iframe:<\/p>\n<pre class=\"lang:css gutter:true start:1\">setContent(content: string): void {\r\n\r\nif (!content) {\r\n\r\nthis.iframe.srcdoc = \"\";\r\n\r\nreturn;\r\n\r\n}\r\n\r\nthis.iframe.srcdoc = content;\r\n\r\n}<\/pre>\n<p>This is what makes interactive charts possible. The entire HTML page\u00a0\u00a0 with inline JavaScript, CSS animations, and event handlers\u00a0\u00a0 runs inside a sandboxed iframe. Hover effects, filter toggles\u00a0\u00a0 everything works because it&#8217;s a living HTML page, not a static image.<\/p>\n<p><strong>A Practical Issue We Hit: Binding the Record ID<\/strong><\/p>\n<p>While setting up the control on the form, we noticed that the PCF manifest declares entityId (Record ID) as a SingleLine.Text input property. Since primary keys are GUID values, they do not appear in the text field dropdown during form configuration. when configuring the control on the form. To handle this, we made a small tweak in validationHandler.ts to directly read the record ID from the form context at runtime instead of relying on the bound field value:<\/p>\n<pre class=\"lang:css gutter:true start:1\">let recordId = this.context.parameters.entityId.raw ?? \"\";\r\n\r\nconst dummyGuid = \"00000000-0000-0000-0000-000000000000\";\r\n\r\nif (!recordId || !this.guidRegex.test(recordId) || recordId === dummyGuid) {\r\n\r\ntry {\r\n\r\nconst xrmPage = (Xrm as unknown as {\r\n\r\nPage?: { data?: { entity?: { getId?: () =&gt; string } } }\r\n\r\n})?.Page;\r\n\r\nconst xrmId = xrmPage?.data?.entity?.getId?.()\r\n\r\n?.replace(\/[{}]\/g, \"\") ?? \"\";\r\n\r\n\u00a0\r\n\r\nif (xrmId &amp;&amp; this.guidRegex.test(xrmId)) {\r\n\r\nrecordId = xrmId;\r\n\r\n} else {\r\n\r\nerrors.push(this.context.resources.getString(\"Error_RecordId_Required\"));\r\n\r\n}\r\n\r\n} catch {\r\n\r\nerrors.push(this.context.resources.getString(\"Error_RecordId_Required\"));\r\n\r\n}\r\n\r\n}<\/pre>\n<p>With this in place, you set Record ID to 00000000-0000-0000-0000-000000000000 in the form designer, and the control reads the actual record ID at runtime from Xrm.<\/p>\n<h3>Things to Keep in Mind<\/h3>\n<p>A few important observations from working with this setup in a live environment:<\/p>\n<p><strong>Response time varies<\/strong>: The Predict call involves server-side Python execution so expect 5\u201315 seconds depending on data volume and prompt complexity.<\/p>\n<p><strong>AI Builder capacity matters<\/strong>: Each Predict call consumes AI Builder credits. For a high-traffic org, this adds up. Monitor consumption in the Power Platform Admin Center.<\/p>\n<p><strong>Prompt engineering matters<\/strong>: Getting the chart to look right proper bar sizing, tooltip positioning, responsive layout took several iterations. The PCF code itself didn&#8217;t need changes; all the tuning was in the prompt text.<\/p>\n<p><strong>One PCF, multiple purposes<\/strong>The same deployed control can support entirely different use cases simply by changing the Model ID and prompt configuration. Swap the Model ID on a different form, point it to a different prompt, and you get entirely different output. The control is genuinely scenario-agnostic.<\/p>\n<h3>Conclusion:<\/h3>\n<p>Through this implementation, we learned that the Code Interpreter PCF control is not just limited to charts or proposals. Any scenario where you need live, context-aware content generated directly on a Dynamics 365 form is a valid use case. Think case summaries on a Case record, contract drafts on a Quote record, or account health reports on an Account record. As long as the prompt has access to the right Dataverse tables and Code Interpreter is enabled, the same control can support completely different experiences simply by swapping the Model ID.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>We spend a lot of time working inside Dynamics 365 CRM forms. Accounts, Opportunities, Cases and other commonly used entities. One area that always had room for improvement was how data gets presented within those forms. For example, if a sales manager wants to see the pipeline breakdown for a specific Account, they typically have\u2026 <span class=\"read-more\"><a href=\"https:\/\/www.inogic.com\/blog\/2026\/05\/how-to-build-ai-generated-charts-and-pdfs-in-dynamics-365-using-a-pcf-control\/\">Read More &raquo;<\/a><\/span><\/p>\n","protected":false},"author":15,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3357,2361],"tags":[3358],"class_list":["post-44658","post","type-post","status-publish","format-standard","hentry","category-pcf-control","category-technical","tag-build-ai-generated-charts-and-pdfs"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/posts\/44658","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/users\/15"}],"replies":[{"embeddable":true,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/comments?post=44658"}],"version-history":[{"count":5,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/posts\/44658\/revisions"}],"predecessor-version":[{"id":44681,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/posts\/44658\/revisions\/44681"}],"wp:attachment":[{"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/media?parent=44658"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/categories?post=44658"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/tags?post=44658"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}