What gets generated
dynamics-365/
├── Solution/
│ ├── solution.xml # Dataverse solution manifest: publisher, version,
│ │ # components list
│ └── customizations.xml # Entity / attribute metadata
├── Plugins/
│ ├── {Entity}Plugin.cs # IPlugin: Execute(), service context, service factory
│ ├── {Entity}Plugin.csproj # Project: Microsoft.CrmSdk.CoreAssemblies
│ ├── AuditPlugin.cs # Emitted when audit_trail capability selected
│ └── {Entity}StateMachinePlugin.cs # Emitted when state machine transitions declared
├── PCFControls/
│ └── {Entity}Control/
│ ├── {entity}Control.tsx # React + Fluent UI PCF component
│ ├── ControlManifest.Input.xml # Manifest: type-group, property, resources
│ └── index.ts # PCF entry point: init, updateView, destroy
├── PowerAutomate/
│ └── {Entity}Approval.json # Power Automate cloud flow: trigger → approval → update
├── SecurityRoles/
│ └── {AppName}_User.xml # Security role: entity + field privileges
└── WebResources/
└── {AppName}.webapi.ts # TypeScript Web API service for client-side calls
IPlugin with correct service resolution
// Plugins/ProductPlugin.cs
public class ProductPlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider
.GetService(typeof(IPluginExecutionContext));
var serviceFactory = (IOrganizationServiceFactory)serviceProvider
.GetService(typeof(IOrganizationServiceFactory));
var service = serviceFactory.CreateOrganizationService(context.UserId);
var tracingService = (ITracingService)serviceProvider
.GetService(typeof(ITracingService));
if (context.MessageName != "Create" || context.Stage != 40) return;
if (context.InputParameters.Contains("Target") &&
context.InputParameters["Target"] is Entity target)
{
// business logic here
}
}
}
This is the correct pattern: resolve services from IServiceProvider, check message name and stage before acting, use ITracingService for debugging. A plugin that resolves IOrganizationService using context.InitiatingUserId instead of context.UserId bypasses the calling user's security — a common mistake that Archiet avoids.
State machine plugin
When your genome declares state transitions, the plugin emits a pre-operation stage guard:
public class ProductStateMachinePlugin : IPlugin
{
private static readonly Dictionary<string, Dictionary<string, string>> Transitions
= new()
{
["Draft"] = new() { ["submit"] = "Pending Review" },
["Pending Review"] = new() { ["approve"] = "Active",
["reject"] = "Draft" },
};
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider
.GetService(typeof(IPluginExecutionContext));
var target = (Entity)context.InputParameters["Target"];
if (!target.Attributes.TryGetValue("new_status", out var statusVal)) return;
var newStatus = statusVal?.ToString();
var preImage = context.PreEntityImages["PreImage"];
var currentStatus = preImage.GetAttributeValue<string>("new_status");
if (!Transitions.TryGetValue(currentStatus, out var allowed) ||
!allowed.ContainsValue(newStatus))
{
throw new InvalidPluginExecutionException(
$"Transition to '{newStatus}' not allowed from '{currentStatus}'.");
}
}
}
PCF control (React + Fluent UI)
// PCFControls/ProductControl/productControl.tsx
import * as React from 'react';
import { Dropdown } from '@fluentui/react';
interface Props { value: string; onChange: (v: string) => void; }
export const ProductControl: React.FC<Props> = ({ value, onChange }) => (
<Dropdown
selectedKey={value}
options={[
{ key: 'Draft', text: 'Draft' },
{ key: 'Active', text: 'Active' },
]}
onChange={(_, opt) => opt && onChange(opt.key as string)}
/>
);
Power Automate flow definition
{
"definition": {
"triggers": {
"When_record_is_updated": {
"type": "OpenApiConnectionWebhook",
"inputs": {
"host": { "connectionName": "shared_commondataserviceforapps" },
"parameters": {
"subscriptionRequest/entityname": "new_product",
"subscriptionRequest/filteringattributes": "new_status"
}
}
}
},
"actions": {
"Start_and_wait_for_an_approval": {
"type": "OpenApiConnection",
"inputs": {
"host": { "connectionName": "shared_approvals" },
"parameters": {
"approvalType": "Approve/Reject - First to respond",
"title": "Product approval required",
"assignedTo": "@triggerOutputs()?['body/_ownerid_value@OData.Community.Display.V1.FormattedValue']"
}
}
}
}
}
}
What ships in docs/
docs/decisions/ADR-dynamics-plugin-vs-flow.md— when to use a synchronous plugin vs a Power Automate flow vs a Business Rule, with the latency and complexity tradeoffs documenteddocs/compliance/dynamics-security-model.md— security role privileges, record ownership, business unit hierarchy, field-level securitydocs/runbooks/dynamics-solution-import.md— managed vs unmanaged solution, import order, dependency resolution
Internal links
- Salesforce Apex integration for the Salesforce platform generator
- SAP CAP integration for SAP BTP custom application generation
CTA
Try it — free plan, no credit card. archiet.com.
Generate a Dynamics 365 project. Open the plugin, the solution manifest, and the security role XML. Check whether it's the shape your Power Platform team would deploy.