Microsoft Dynamics 365
Connect Dynamics 365 Business Central and Finance & Operations to Certyo for blockchain-backed authenticity certificates on every shipment, inventory movement, and sales transaction.
Overview
Dynamics 365 covers two distinct product families with different integration surfaces. This guide provides production-ready patterns for both:
- Business Central — AL extensions, OData v4 API Pages, and webhooks for SMB customers running Business Central Online or on-premises.
- Finance & Operations (F&O) — Dataverse Dual-Write, Business Events, and Data Entity OData endpoints for enterprise customers running Supply Chain Management, Commerce, or Finance.
Prerequisites
- Azure tenant with Microsoft Entra ID (formerly Azure AD)
- Dynamics 365 Business Central or Finance & Operations subscription
- Certyo API key from the backoffice (
certyo_sk_live_...) - Azure Service Bus namespace (Standard tier or higher)
- Azure Functions runtime (v4, .NET 8 isolated worker)
Architecture
Both integration patterns follow the same event-driven flow. The ERP emits a domain event, Azure infrastructure decouples the systems, and a serverless function calls Certyo:
┌─────────────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐
│ Dynamics 365 │ │ Azure │ │ Azure Function │ │ Certyo API │
│ │────>│ Service Bus │────>│ │────>│ │
│ Business Central │ │ │ │ CertyoIngest │ │ POST │
│ or F&O │ │ certyo-records │ │ Function │ │ /api/v1/ │
│ │ │ queue │ │ │ │ records │
└─────────────────────┘ └──────────────────┘ └──────────────────┘ └──────────────┘
│ │
│ Business Central: │ Polls Certyo for
│ AL Codeunit → HttpClient │ anchor status, then
│ or Webhook subscription │ writes certificate
│ │ back to D365 via
│ Finance & Operations: │ OData PATCH
│ Business Event → Service Bus │
│ or Dataverse Dual-Write trigger │
└─────────────────────────────────────────────────────┘Business Central Integration
Business Central exposes data through OData v4 API Pages and supports AL extensions for custom logic. The recommended pattern is to fire a Service Bus message from an AL Codeunit when a Sales Shipment is posted.
Option A: AL Extension (recommended)
Subscribe to the OnAfterPostSalesShipment event, build the Certyo record payload, and send it to Azure Service Bus. The Azure Function picks it up and calls Certyo.
Option B: OData v4 API Page + Power Automate
Expose a custom API Page with the fields Certyo needs, then use Power Automate to poll for new shipments and forward them. This is lower-code but introduces polling latency.
Finance & Operations Integration
F&O provides Business Events and Dataverse Dual-Write for real-time integration. The recommended pattern uses Business Events to push inventory transaction events to Azure Service Bus, where an Azure Function transforms and forwards them to Certyo.
Dataverse Dual-Write pattern
When a Goods Receipt or Inventory Transfer is posted in F&O, Dual-Write synchronizes the entity to Dataverse. A Power Automate flow or Dataverse plugin detects the row change and calls Certyo. This pattern works well when you already use Dual-Write for other integrations.
Business Events pattern (recommended)
Register a Business Event on the InventTransPosting entity. Configure the endpoint to deliver to Azure Service Bus. The Azure Function consumes the message and calls Certyo with the material document details.
Code Samples
using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using System.Net.Http.Json;
using System.Text.Json;
namespace Certyo.Dynamics365.Functions;
public sealed class CertyoIngestFunction
{
private readonly HttpClient _http;
private readonly ILogger<CertyoIngestFunction> _logger;
public CertyoIngestFunction(
IHttpClientFactory httpClientFactory,
ILogger<CertyoIngestFunction> logger)
{
_http = httpClientFactory.CreateClient("Certyo");
_logger = logger;
}
[Function("CertyoIngest")]
public async Task Run(
[ServiceBusTrigger("certyo-records", Connection = "ServiceBusConnection")]
ServiceBusReceivedMessage message,
CancellationToken cancellationToken)
{
var d365Event = JsonSerializer.Deserialize<D365ShipmentEvent>(
message.Body.ToString(),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (d365Event is null)
{
_logger.LogWarning("Received null event, skipping");
return;
}
var certyoRecord = new
{
tenantId = Environment.GetEnvironmentVariable("CERTYO_TENANT_ID"),
database = "dynamics365",
collection = d365Event.EntityName,
recordId = d365Event.DocumentNo,
recordVersion = d365Event.Version ?? "1",
operationType = "insert",
sourceTimestamp = d365Event.PostingDate.ToString("O"),
idempotencyKey = $"{d365Event.DocumentNo}-{d365Event.Version ?? "1"}-{d365Event.PostingDate:yyyy-MM-dd}",
recordPayload = new
{
documentNo = d365Event.DocumentNo,
entityName = d365Event.EntityName,
sellToCustomerNo = d365Event.SellToCustomerNo,
sellToCustomerName = d365Event.SellToCustomerName,
shipToCode = d365Event.ShipToCode,
postingDate = d365Event.PostingDate,
shipmentMethodCode = d365Event.ShipmentMethodCode,
trackingNo = d365Event.TrackingNo,
lines = d365Event.Lines.Select(l => new
{
lineNo = l.LineNo,
itemNo = l.ItemNo,
description = l.Description,
quantity = l.Quantity,
unitOfMeasureCode = l.UnitOfMeasureCode,
lotNo = l.LotNo,
serialNo = l.SerialNo
})
}
};
var response = await _http.PostAsJsonAsync(
"/api/v1/records",
certyoRecord,
cancellationToken);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogError(
"Certyo ingestion failed for {DocumentNo}: {Status} {Body}",
d365Event.DocumentNo, response.StatusCode, body);
throw new InvalidOperationException(
$"Certyo returned {response.StatusCode}");
}
var result = await response.Content
.ReadFromJsonAsync<JsonElement>(cancellationToken);
_logger.LogInformation(
"Ingested {DocumentNo} -> recordHash: {Hash}",
d365Event.DocumentNo,
result.GetProperty("recordHash").GetString());
}
}
public record D365ShipmentEvent
{
public string DocumentNo { get; init; } = "";
public string EntityName { get; init; } = "SalesShipmentHeader";
public string SellToCustomerNo { get; init; } = "";
public string SellToCustomerName { get; init; } = "";
public string ShipToCode { get; init; } = "";
public DateTime PostingDate { get; init; }
public string ShipmentMethodCode { get; init; } = "";
public string TrackingNo { get; init; } = "";
public string? Version { get; init; }
public List<ShipmentLine> Lines { get; init; } = new();
}
public record ShipmentLine
{
public int LineNo { get; init; }
public string ItemNo { get; init; } = "";
public string Description { get; init; } = "";
public decimal Quantity { get; init; }
public string UnitOfMeasureCode { get; init; } = "";
public string? LotNo { get; init; }
public string? SerialNo { get; init; }
}This Power Automate flow definition triggers when a Dataverse row changes on the msdyn_inventorytransaction entity (F&O Dual-Write scenario) and calls Certyo. Import this JSON in Power Automate > My Flows > Import.
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"triggers": {
"When_a_row_is_added_or_modified": {
"type": "OpenApiConnectionWebhook",
"inputs": {
"host": {
"connectionName": "shared_commondataserviceforapps",
"operationId": "SubscribeWebhookTrigger",
"apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps"
},
"parameters": {
"subscriptionRequest/message": 4,
"subscriptionRequest/entityname": "msdyn_inventorytransaction",
"subscriptionRequest/scope": 4,
"subscriptionRequest/filterexpression": "msdyn_transactiontype eq 100000000"
}
}
}
},
"actions": {
"Initialize_Certyo_Payload": {
"type": "Compose",
"inputs": {
"tenantId": "@{parameters('CertyoTenantId')}",
"database": "dynamics365-fo",
"collection": "InventoryTransactions",
"recordId": "@{triggerOutputs()?['body/msdyn_inventorytransactionid']}",
"operationType": "insert",
"sourceTimestamp": "@{triggerOutputs()?['body/modifiedon']}",
"idempotencyKey": "@{concat(triggerOutputs()?['body/msdyn_inventorytransactionid'], '-', formatDateTime(triggerOutputs()?['body/modifiedon'], 'yyyy-MM-dd'))}",
"recordPayload": {
"transactionId": "@{triggerOutputs()?['body/msdyn_inventorytransactionid']}",
"itemNumber": "@{triggerOutputs()?['body/msdyn_itemnumber']}",
"productName": "@{triggerOutputs()?['body/msdyn_productname']}",
"transactionType": "@{triggerOutputs()?['body/msdyn_transactiontype']}",
"quantity": "@{triggerOutputs()?['body/msdyn_quantity']}",
"unit": "@{triggerOutputs()?['body/msdyn_unitid']}",
"warehouse": "@{triggerOutputs()?['body/msdyn_warehouseid']}",
"batchNumber": "@{triggerOutputs()?['body/msdyn_batchnumber']}",
"serialNumber": "@{triggerOutputs()?['body/msdyn_serialnumber']}",
"inventorySiteId": "@{triggerOutputs()?['body/msdyn_inventorysiteid']}",
"transactionDate": "@{triggerOutputs()?['body/msdyn_transactiondate']}"
}
},
"runAfter": {}
},
"Call_Certyo_Ingest": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://www.certyos.com/api/v1/records",
"headers": {
"X-API-Key": "@{parameters('CertyoApiKey')}",
"Content-Type": "application/json"
},
"body": "@outputs('Initialize_Certyo_Payload')"
},
"runAfter": {
"Initialize_Certyo_Payload": ["Succeeded"]
},
"runtimeConfiguration": {
"contentTransfer": { "transferMode": "Chunked" }
}
},
"Check_Response": {
"type": "If",
"expression": {
"and": [
{ "equals": ["@outputs('Call_Certyo_Ingest')['statusCode']", 202] }
]
},
"actions": {
"Log_Success": {
"type": "Compose",
"inputs": "Certyo accepted record @{triggerOutputs()?['body/msdyn_inventorytransactionid']} with hash @{body('Call_Certyo_Ingest')?['recordHash']}"
}
},
"else": {
"actions": {
"Log_Failure": {
"type": "Compose",
"inputs": "Certyo rejected record: @{body('Call_Certyo_Ingest')}"
}
}
},
"runAfter": {
"Call_Certyo_Ingest": ["Succeeded", "Failed"]
}
}
},
"parameters": {
"CertyoTenantId": {
"type": "String",
"defaultValue": ""
},
"CertyoApiKey": {
"type": "SecureString",
"defaultValue": ""
}
}
}
}This AL codeunit subscribes to the Sales Shipment posting event in Business Central and sends the shipment data to Azure Service Bus. The Azure Function (shown in the C# tab) picks it up and forwards to Certyo.
// AL Extension for Business Central
// Language: AL (displayed as TypeScript — no AL syntax option)
codeunit 50100 "Certyo Shipment Publisher"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnAfterPostSalesDoc', '', false, false)]
local procedure OnAfterPostSalesDoc(
var SalesHeader: Record "Sales Header";
var GenJnlPostLine: Codeunit "Gen. Jnl.-Post Line";
SalesShptHdrNo: Code[20];
RetRcpHdrNo: Code[20];
SalesInvHdrNo: Code[20];
SalesCrMemoHdrNo: Code[20])
var
SalesShipmentHeader: Record "Sales Shipment Header";
SalesShipmentLine: Record "Sales Shipment Line";
ServiceBusHelper: Codeunit "Certyo Service Bus Helper";
JsonPayload: JsonObject;
LinesArray: JsonArray;
LineObject: JsonObject;
begin
if SalesShptHdrNo = '' then
exit;
SalesShipmentHeader.Get(SalesShptHdrNo);
JsonPayload.Add('DocumentNo', SalesShipmentHeader."No.");
JsonPayload.Add('EntityName', 'SalesShipmentHeader');
JsonPayload.Add('SellToCustomerNo', SalesShipmentHeader."Sell-to Customer No.");
JsonPayload.Add('SellToCustomerName', SalesShipmentHeader."Sell-to Customer Name");
JsonPayload.Add('ShipToCode', SalesShipmentHeader."Ship-to Code");
JsonPayload.Add('PostingDate', Format(SalesShipmentHeader."Posting Date", 0, '<Year4>-<Month,2>-<Day,2>'));
JsonPayload.Add('ShipmentMethodCode', SalesShipmentHeader."Shipment Method Code");
JsonPayload.Add('TrackingNo', SalesShipmentHeader."Package Tracking No.");
SalesShipmentLine.SetRange("Document No.", SalesShptHdrNo);
if SalesShipmentLine.FindSet() then
repeat
Clear(LineObject);
LineObject.Add('LineNo', SalesShipmentLine."Line No.");
LineObject.Add('ItemNo', SalesShipmentLine."No.");
LineObject.Add('Description', SalesShipmentLine.Description);
LineObject.Add('Quantity', SalesShipmentLine.Quantity);
LineObject.Add('UnitOfMeasureCode', SalesShipmentLine."Unit of Measure Code");
LinesArray.Add(LineObject);
until SalesShipmentLine.Next() = 0;
JsonPayload.Add('Lines', LinesArray);
ServiceBusHelper.SendToQueue('certyo-records', JsonPayload);
end;
}
codeunit 50101 "Certyo Service Bus Helper"
{
procedure SendToQueue(QueueName: Text; Payload: JsonObject)
var
HttpClient: HttpClient;
HttpContent: HttpContent;
HttpHeaders: HttpHeaders;
HttpResponseMessage: HttpResponseMessage;
ServiceBusUri: Text;
SasToken: Text;
PayloadText: Text;
begin
ServiceBusUri := GetServiceBusUri(QueueName);
SasToken := GenerateSasToken(QueueName);
Payload.WriteTo(PayloadText);
HttpContent.WriteFrom(PayloadText);
HttpContent.GetHeaders(HttpHeaders);
HttpHeaders.Remove('Content-Type');
HttpHeaders.Add('Content-Type', 'application/json');
HttpHeaders.Add('Authorization', SasToken);
HttpClient.Post(ServiceBusUri, HttpContent, HttpResponseMessage);
if not HttpResponseMessage.IsSuccessStatusCode() then
Error('Failed to send to Service Bus: %1', HttpResponseMessage.ReasonPhrase());
end;
local procedure GetServiceBusUri(QueueName: Text): Text
var
Setup: Record "Certyo Setup";
begin
Setup.Get();
exit(StrSubstNo('https://%1.servicebus.windows.net/%2/messages',
Setup."Service Bus Namespace", QueueName));
end;
local procedure GenerateSasToken(QueueName: Text): Text
var
Setup: Record "Certyo Setup";
CryptographyMgmt: Codeunit "Cryptography Management";
Uri: Text;
Expiry: Integer;
StringToSign: Text;
Signature: Text;
begin
Setup.Get();
Uri := GetServiceBusUri(QueueName);
Expiry := GetUnixTimestamp() + 3600;
StringToSign := Uri + '\n' + Format(Expiry);
Signature := CryptographyMgmt.GenerateHashAsBase64String(
StringToSign, Setup."Service Bus SAS Key",
2); // HMACSHA256
exit(StrSubstNo('SharedAccessSignature sr=%1&sig=%2&se=%3&skn=%4',
Uri, Signature, Expiry, Setup."Service Bus SAS Key Name"));
end;
local procedure GetUnixTimestamp(): Integer
begin
exit(Round((CurrentDateTime() - CreateDateTime(19700101D, 0T)) / 1000, 1));
end;
}Azure Function Configuration
Register the Certyo named HttpClient in Program.cs so the function can call the API with the correct base address and API key header:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
services.AddHttpClient("Certyo", client =>
{
client.BaseAddress = new Uri("https://www.certyos.com");
client.DefaultRequestHeaders.Add(
"X-API-Key",
Environment.GetEnvironmentVariable("CERTYO_API_KEY"));
});
})
.Build();
host.Run();Required app settings
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"ServiceBusConnection": "<your-service-bus-connection-string>",
"CERTYO_API_KEY": "certyo_sk_live_abc123...",
"CERTYO_TENANT_ID": "your-tenant-id"
}
}Authentication
Dynamics 365 side
Register an App Registration in Microsoft Entra ID with the Dynamics 365 Business Central or Dynamics 365 Finance and Operations API permission. Use OAuth 2.0 client credentials flow to obtain a bearer token:
curl -X POST "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id={app-id}" \
-d "client_secret={client-secret}" \
-d "scope=https://api.businesscentral.dynamics.com/.default" \
-d "grant_type=client_credentials"For Finance & Operations, replace the scope with https://your-environment.operations.dynamics.com/.default.
Certyo side
Certyo uses a simple API key in the X-API-Key header. Store the key in Azure Key Vault and reference it from your Function App settings using Key Vault references:
# Key Vault reference syntax in Azure Function app settings
CERTYO_API_KEY=@Microsoft.KeyVault(SecretUri=https://your-vault.vault.azure.net/secrets/certyo-api-key/)Rate Limits & Batching Strategy
Dynamics 365 enforces API rate limits that you must account for in your integration:
- Business Central: 6,000 OData API requests per 5-minute sliding window per environment. Exceeding this returns 429 Too Many Requests.
- Finance & Operations: 6,000 requests per 5-minute window per user, with a priority-based throttling system at peak load.
- Certyo: The single-record endpoint has generous limits, but for high-volume scenarios, use POST /api/v1/records/bulk (up to 1,000 records per request).
Recommended batching strategy: If your D365 environment processes more than 100 shipments per minute, accumulate events in the Service Bus queue and have the Azure Function use a batch trigger with MaxMessageBatchSize = 100. Map each batch to a single POST /api/v1/records/bulk call:
[Function("CertyoBatchIngest")]
public async Task RunBatch(
[ServiceBusTrigger("certyo-records", Connection = "ServiceBusConnection",
IsBatched = true)]
ServiceBusReceivedMessage[] messages,
CancellationToken cancellationToken)
{
var records = messages
.Select(m => JsonSerializer.Deserialize<D365ShipmentEvent>(m.Body.ToString()))
.Where(e => e is not null)
.Select(e => new
{
database = "dynamics365",
collection = e!.EntityName,
recordId = e.DocumentNo,
recordVersion = e.Version ?? "1",
operationType = "insert",
sourceTimestamp = e.PostingDate.ToString("O"),
idempotencyKey = $"{e.DocumentNo}-{e.Version ?? "1"}-{e.PostingDate:yyyy-MM-dd}",
recordPayload = new
{
documentNo = e.DocumentNo,
entityName = e.EntityName,
sellToCustomerNo = e.SellToCustomerNo,
sellToCustomerName = e.SellToCustomerName,
postingDate = e.PostingDate,
trackingNo = e.TrackingNo
}
})
.ToList();
var bulkPayload = new
{
tenantId = Environment.GetEnvironmentVariable("CERTYO_TENANT_ID"),
records
};
var response = await _http.PostAsJsonAsync(
"/api/v1/records/bulk", bulkPayload, cancellationToken);
response.EnsureSuccessStatusCode();
_logger.LogInformation("Bulk ingested {Count} records", records.Count);
}Verification & Write-Back
After records are anchored (typically 60-90 seconds), you can verify them and write the certificate status back to Dynamics 365 custom fields. Use a timer-triggered Azure Function that polls Certyo and patches D365 via OData:
[Function("CertyoVerifyWriteBack")]
public async Task RunVerify(
[TimerTrigger("0 */2 * * * *")] TimerInfo timer,
CancellationToken cancellationToken)
{
var tenantId = Environment.GetEnvironmentVariable("CERTYO_TENANT_ID");
// Query Certyo for recently ingested records pending verification
var queryResponse = await _http.GetAsync(
$"/api/v1/records?tenantId={tenantId}&database=dynamics365&status=Anchored&verifiedInErp=false&limit=50",
cancellationToken);
var records = await queryResponse.Content
.ReadFromJsonAsync<RecordQueryResult>(cancellationToken);
foreach (var record in records?.Items ?? [])
{
// Verify the record on-chain
var verifyResponse = await _http.PostAsJsonAsync(
"/api/v1/verify/record",
new
{
tenantId,
database = record.Database,
collection = record.Collection,
recordId = record.RecordId,
payload = record.RecordPayload
},
cancellationToken);
var verification = await verifyResponse.Content
.ReadFromJsonAsync<JsonElement>(cancellationToken);
if (verification.GetProperty("verified").GetBoolean())
{
// Write certificate data back to D365 via OData
var d365Token = await GetD365AccessToken(cancellationToken);
var patchPayload = new
{
certyoVerified = true,
certyoRecordHash = verification.GetProperty("snapshotHash").GetString(),
certyoMerkleRoot = verification.GetProperty("merkleRoot").GetString(),
certyoPolygonTx = verification.GetProperty("onChainProof")
.GetProperty("polygonScanEventUrl").GetString(),
certyoVerifiedAt = DateTime.UtcNow
};
using var patchRequest = new HttpRequestMessage(
HttpMethod.Patch,
$"https://api.businesscentral.dynamics.com/v2.0/{tenantId}/Production/api/v2.0/companies({{companyId}})/salesShipments({{shipmentId}})");
patchRequest.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", d365Token);
patchRequest.Content = JsonContent.Create(patchPayload);
await new HttpClient().SendAsync(patchRequest, cancellationToken);
_logger.LogInformation(
"Wrote certificate back to D365 for {RecordId}: {TxUrl}",
record.RecordId,
patchPayload.certyoPolygonTx);
}
}
}Testing
- Deploy the Azure Function to a staging slot and configure it with your Certyo sandbox key. (
certyo_sk_test_...) - Post a Sales Shipment in Business Central (or a Goods Receipt in F&O) and confirm the message appears in the Service Bus queue.
- Verify the Azure Function processed the message by checking Application Insights logs for the recordHash output.
- Wait 60-90 seconds, then call POST /api/v1/verify/record with the shipment data to confirm on-chain anchoring.
- Check the D365 Sales Shipment record for the populated Certyo custom fields.
AI Integration Skill
Download a skill file that enables AI agents to generate working Microsoft Dynamics 365 + Certyo integration code for any language or framework.
What's inside
- Authentication — OAuth 2.0 client credentials with Entra ID and Key Vault setup
- Architecture — D365 event → Azure Service Bus → Azure Function → Certyo pipeline
- Field mapping — SalesShipmentHeader, Item, and Line fields to Certyo record schema
- Code examples — C# Azure Function, AL extension for Business Central, Power Automate flow
- Verification — TimerTrigger polling and OData PATCH write-back to D365 custom fields
- Rate limiting — D365 OData 6,000 req/5-min window and batching strategies
How to use
Claude Code
Place the file in your project's .claude/commands/ directory, then use it as a slash command:
# Download the skill file
mkdir -p .claude/commands
curl -o .claude/commands/certyo-dynamics365.md \
https://www.certyos.com/developers/skills/certyo-dynamics365-skill.md
# Use it in Claude Code
/certyo-dynamics365 "Generate an Azure Function that ingests D365 shipment events into Certyo"Cursor / Copilot / Any AI Agent
Add the file to your project root or attach it to a conversation. The AI agent will use the Microsoft Dynamics 365-specific patterns, field mappings, and code examples to generate correct integration code.
# Add to your project
curl -o CERTYO_DYNAMICS365.md \
https://www.certyos.com/developers/skills/certyo-dynamics365-skill.md
# Then in your AI agent:
"Using the Certyo Microsoft Dynamics 365 spec in CERTYO_DYNAMICS365.md,
generate an azure function that ingests d365 shipment events into certyo"CLAUDE.md Context File
Append the skill file to your project's CLAUDE.md so every Claude conversation has Microsoft Dynamics 365 + Certyo context automatically.
# Append to your project's CLAUDE.md
echo "" >> CLAUDE.md
echo "## Certyo Microsoft Dynamics 365 Integration" >> CLAUDE.md
cat CERTYO_DYNAMICS365.md >> CLAUDE.md