How We Built a Project Operations to Business Central Integration Using Dataverse Plugins - CloudFronts

How We Built a Project Operations to Business Central Integration Using Dataverse Plugins

01Summary

Project-based businesses often run their projects in Dynamics 365 Project Operations and their finance in Dynamics 365 Business Central. The two systems do not talk to each other out of the box, so every customer, project, timesheet, expense and invoice ends up being typed into both, and the numbers finance reports on are always a step behind the project teams.

CloudFronts built an integration that closes this gap using Dataverse plugins: small pieces of C# code that run inside Project Operations every time a record is saved. When someone creates a customer, sets up a project, approves a timesheet or confirms an invoice, the plugin sends that record straight to the matching Business Central web service and writes the Business Central number back onto it. Before material is used on a project, it looks up how much stock Business Central holds at that location. Anything that fails is saved in an Error Log with the exact data that was sent.

To keep the examples concrete, this post follows a sample scenario: Wexford County Council orders a satellite broadband installation at its Castlemoyle and Bunclody water treatment sites, and the project runs from the first lead to the final invoice across both systems.

Table of Contents

02Business Challenges

Project Operations and Business Central each work well on their own. The trouble is the gap between them: nothing entered in Project Operations reaches Business Central unless someone types it in again. Without an integration, teams run into the same problems:

  • Everything is entered twice. A new customer or project is created in Project Operations, then created again in Business Central. Every later change has to be repeated by hand.
  • Master data drifts apart. Countries, postcodes, currencies, payment terms, dimensions and work types have to match in both systems. When one side changes and the other does not, records fail to post.
  • Approved work reaches finance late. Approved timesheets, expenses and material only arrive in Business Central after someone exports and imports them, so project costs in finance are always behind.
  • Material is used without checking stock. Site teams log material against a project without knowing whether Business Central actually has it at that location.
  • Invoices are keyed twice. A confirmed project invoice has to be rebuilt in Business Central, line by line, before finance can post it.
  • Errors are hard to trace. When a record does not match, nobody can see what was sent or why Business Central rejected it.

03Solution Overview

CloudFronts built one main plugin, RequestProcessor, and registered it on every Project Operations table that finance needs. Two small helper plugins handle the stock check for material. Whenever one of those records is created, updated or deleted, Dataverse runs the plugin. The plugin works out which table the record came from, turns it into the shape Business Central expects, and sends it to the matching Business Central web service.

When Business Central replies, the plugin stores the Business Central number on the Project Operations record, such as the BC customer number or the project code. Next time the record changes, the plugin uses that number to update the same record in Business Central instead of creating a new one.

All the settings the plugin needs, such as where to sign in, which web service to call for each table, and which user is the integration user, live in a single Integration Configuration record in Dataverse. Moving from a test company to the live one is a change to that record, not to the code.

04Technical Approach

How It Works

Every save takes the same path through the plugin:

  1. A record is saved in Project OperationsFor example, the project manager approves a week of time entries on the Wexford County Council broadband project.
  2. RequestProcessor loads its settingsIt reads the Integration Configuration record. If the record is missing, the plugin stops quietly instead of failing the user’s save.
  3. It skips its own changesIf the record was last modified by the integration user, the plugin stops. This keeps it from running again on the Business Central number it has just written back.
  4. The handler for that table maps the recordIt checks the record’s status, so only approved entries or confirmed invoices go further, and copies the fields into the JSON shape Business Central expects.
  5. Business Central creates or updates the recordPOST creates a new record; PUT updates the one already linked by its Business Central number.
  6. The result is savedOn success, the Business Central number is written back onto the Project Operations record. On failure, an Error Log record is created with the details.

What Moves Between the Systems

The plugin handles 20 Project Operations tables. They fall into five groups, in the order a project normally runs.

GroupSaved in Project OperationsCreated in Business CentralWhen It Is Sent
CustomersAccountCustomer, or Vendor when the account type is VendorCreate and update; a delete blocks the record in BC instead of removing it
CustomersContactContactCreate, update and delete
Master dataCountry code, postcode, currency, payment termsThe same lists in BCCreate and update
Master dataDimension, dimension value, work type, resource group, bookable resourceDimensions, work types, resource groups, resourcesCreate and update
Project setupProjectProject; the BC project code is written backCreate and update; projects cannot be deleted
Project setupProject taskProject taskCreate and update
Project setupResource assignment and estimate lineProject Planning Lines (the budget)When the line is created with its task, quantity and amount
ActualsTime entryProject Journal line (Resource)When the entry is approved
ActualsMaterial usageProject Journal line (Item)When the usage is approved or returned
ActualsExpenseProject Journal line (G/L Account)When the expense is approved
BillingProject vendor invoice and its linesPurchase document and linesWhen the vendor invoice is confirmed
BillingProject invoice and its linesSales invoice and linesWhen the invoice is confirmed

What the plugin sends from Project Operations to Business Central

Two rules keep this simple:

  • Everything flows one way. Project Operations is where records start. Business Central receives them and sends back only its own number for each record.
  • Only finished work is sent. Draft timesheets, unapproved expenses and unconfirmed invoices stay in Project Operations. Finance only sees what has been approved.

Here is what that looks like in practice, from customer to invoice:

Account is integrated to BC
An account saved in Project Operations becomes a customer in Business Central
Project is integrated to BC
The project created automatically in Business Central
Resource assignment in BC Project Planning Lines
Resource assignments become the project budget in Business Central
Time entries synced to the BC Project Journal
Approved time entries arrive in the Project Journal ready to post
Vendor invoice synced, BC number updated in PO
A confirmed vendor invoice is sent to Business Central and its number comes back
Invoice synced to BC with its lines
A confirmed project invoice becomes a sales invoice in Business Central

Inside the Plugin, Step by Step

Every save follows the same path through RequestProcessor:

  1. A record is saved. A user creates, updates or deletes a record on one of the 20 registered tables.
  2. Load the settings. The plugin reads the Integration Configuration record. If it is missing, the plugin stops quietly.
  3. Get the full record. For a create, it uses the record being saved. For an update, it uses the post-image (the record as it looks after the save). For a delete, it uses the pre-image (the record as it looked before).
  4. Skip its own changes. If the record was last modified by the integration user named in the settings, the plugin stops.
  5. Pick the right handler. A switch on the table name hands the record to the class for that table: AccountsOperations for accounts, TimeEntriesOperations for time entries, and so on. Each handler checks the record’s status first.
  6. Map and send. The handler copies the fields Business Central needs into a small data class (a DTO), converts it to JSON and sends it to that table’s web service: POST to create, PUT to update.
  7. Write the BC number back. The number Business Central returns is saved on the Project Operations record, so the next change updates the same record in Business Central.
  8. Log anything that fails. If Business Central rejects the call, the error, the web service address, the JSON that was sent and Business Central’s reply are saved as an Error Log record.

The routing is a plain switch, which makes it easy to see what happens for each table:

RequestProcessor.cs (shortened)C#
switch (targetEntity.LogicalName)
{
    case Variables.CRM_ACCOUNT:
        if (message == "create") AccountsOperations.SyncCreatedAccountBC(integrationConfig, targetEntity, service);
        else if (message == "update") AccountsOperations.SyncUpdatedAccountBC(integrationConfig, targetEntity, service);
        else if (message == "delete") AccountsOperations.SyncDeletedAccountBC(integrationConfig, targetEntity, service);
        break;

    case Variables.CRM_PROJECT:
        if (message == "delete")
            throw new InvalidPluginExecutionException("Error: Projects cannot be deleted from Project Operations");
        // create and update are sent to the BC project web service
        break;

    case Variables.CRM_TIMEENTRY:
        var entryStatus = targetEntity.GetAttributeValue<OptionSetValue>(Variables.TIMEENTRY_ENTRYSTATUS);
        if (entryStatus != null && entryStatus.Value == 192350002)   // Approved
            TimeEntriesOperations.SyncTimeEntriesToProjectJournalBC(integrationConfig, targetEntity, service, tracingService);
        break;

    // ... one case for each of the 20 tables
}
BC Invoice Number updated in Project Operations
Step 7 in action: the Business Central number written back onto the record

Setting It Up

  1. Give the integration its own identity. Register an app in Microsoft Entra ID with the Business Central API permission, create a client secret, then add the same app on the Microsoft Entra Applications page in Business Central with only the permissions the integration needs. The plugin signs in as this app, not as a person, so it keeps working when people change roles.
  2. Publish the Business Central web services. Each record type is sent to its own web service: customers, vendors, contacts, projects, project tasks, planning lines, the project journal, sales and purchase invoices, the master-data lists, and item ledger entries for stock.
  3. Fill in the Integration Configuration record. This one Dataverse record holds everything the plugin needs to know (see the table below).
  4. Register the plugin steps. In the Plugin Registration Tool, register RequestProcessor on Create, Update and Delete for each table it should handle, with a post-image for updates and a pre-image for deletes.
SettingWhat It Holds
Token URL, Client ID, Client Secret, Scope, Grant TypeHow the plugin signs in to Business Central
One web service URL per record typeWhere each table is sent, for example the Project or Project Journal web service
Time and Material G/L number, Fixed Price G/L numberWhich G/L account invoice lines post to, by billing type
Integration UserThe Dataverse user the plugin acts as, so it can ignore its own changes

The Integration Configuration record

How the Plugin Signs In and Calls Business Central

Before its first call, the plugin asks Microsoft Entra ID for an access token using the app’s client ID and secret. It keeps the token for the rest of the run, so one save does not sign in again for every call. Each request then carries the token:

Sign-in and callsHTTP
POST <Token URL from Integration Configuration>
     grant_type=client_credentials & client_id=... & client_secret=... & scope=...
→ access token

POST <Project web service URL>             create a project, BC returns its number
PUT  <Project web service URL>('PR00042')  update the same project next time

Checking Stock Before Material Is Used

Material is the one place where Project Operations needs to ask Business Central a question before it sends anything: is this item actually in stock at this location? Two small plugins handle it.

  1. When material is logged, look up the stock. As a user enters the product and location on a Material Usage record, the ProductsOperation plugin reads the item ledger entries in Business Central for that item number and location code. It saves the quantity in a Quantity Available field, so the user can see it on the form.
  2. When the material is approved, compare. When a project approval for material moves to the approved stage, the PreValidationMaterialUsageApproval plugin compares the quantity requested with the quantity available. If there is not enough stock, it writes an entry to the Error Log so the team can follow up.
  3. When approved, post it. RequestProcessor sends the material to the Project Journal in Business Central as an Item line, where posting reduces stock and records the cost against the project.

The stock lookup is a single filtered call to Business Central:

ProductsOperation.cs (shortened)C#
string url = integrationConfig.CF_ITEMLEDGERENTRIESWEBSERVICEURL +
             "?$filter=Item_No eq '" + productNumber + "' and Location_Code eq '" + locationCode + "'";
string response = APIOperations.GetRequestToBC(url, integrationConfig, "fetchProductDetailsFromBC",
                                               targetEntity.LogicalName, targetEntity.Id.ToString(), service);

itemLedgerEntries = (ItemLedgerEntries)APIOperations.ConvertJsonToObject(response, itemLedgerEntries, service);
targetEntity["cf_quantityavailable"] = itemLedgerEntries.Quantity;   // shown on the Material Usage form
Material usage synced to the BC Project Journal
Approved material is posted against the project in Business Central

Guardrails for Real-World Use

  • 🔁No Endless LoopsWhen the plugin writes the Business Central number back, that is itself a save. The plugin checks who made the change, and if it was the integration user, it stops.
  • 🔢Create Once, Then UpdateThe Business Central number stored on each record decides what happens next. No number means create; a number means update that record in Business Central.
  • ⛔Nothing Is Deleted by AccidentDeleting an account blocks the customer or vendor in Business Central instead of removing it. Projects cannot be deleted at all; the user sees a clear message instead.
  • ✅Only Approved Work Reaches FinanceTime entries, expenses and material are sent only when approved, and invoices only when confirmed.
  • 📝Incomplete Data Is Stopped EarlyBefore a time entry or material line is sent, the plugin checks that the project, task, resource or item and quantity are filled in, and tells the user what to fix.
  • 🗂Every Failure Is RecordedEach failed call creates an Error Log record with the method, table, record ID, web service address, the JSON sent and Business Central’s reply.

05Business Impact

  • 20Project Operations tables synced to Business Central
  • 3Plugins: one main plugin and two for the stock check
  • 1Configuration record holding every setting
  • 0Customers, projects, timesheets or invoices typed in twice
  • Records are entered once. Customers, projects, tasks and budgets appear in Business Central as soon as they are saved in Project Operations.
  • Master data stays in step. Currencies, payment terms, dimensions, work types and resources match in both systems, so postings stop failing on missing codes.
  • Project costs in finance are current. Approved time, expenses and material reach the Project Journal when they are approved, not at month end.
  • Invoices are not rebuilt by hand. A confirmed project invoice arrives in Business Central as a sales invoice with its lines, ready for finance to post.
  • Stock is visible where material is logged. Users see the quantity Business Central holds before they use it, and shortfalls are flagged at approval.

Because every failed call is logged with the data that was sent, fixing a sync issue takes minutes instead of a search through two systems.

06Conclusion

Keeping finance up to date with project work is rarely difficult. It is just repetitive: set the customer up again, create the project again, retype the timesheets, rebuild the invoice. A Dataverse plugin takes that repetition away. Each record is sent to Business Central the moment it is saved or approved in Project Operations, the Business Central number comes back onto the record, and anything that fails is logged with the data that was sent.

The same pattern works beyond Project Operations: one plugin that routes by table, a configuration record for settings, the Business Central number stored on each record, and an error log. It applies to any Dataverse system that needs to feed Business Central, such as Dynamics 365 Sales orders, Field Service work orders or Customer Service cases that need billing.

CloudFronts also offers this integration as a ready-to-deploy solution: the Project Operations to Business Central Integration Module on Microsoft Marketplace.

Ready to Connect Project Operations and Business Central?

If your project and finance teams are typing the same data into Dynamics 365 Project Operations and Business Central, connect with CloudFronts for a solution architecture review. We have built plugin-based Project Operations to Business Central integrations and can help you design one around your own process.

Connect with CloudFrontsView on Microsoft Marketplace

About the Author

Cassandra Rodrigues

Cassandra Rodrigues is a Microsoft Dynamics 365 and Power Platform Consultant with four years of experience delivering CRM customization, business-process automation, customer portals, and system integrations. Her work spans Dynamics 365 Sales, Customer Service, Project Operations, Power Apps, Power Automate, Power Pages, Microsoft Dataverse, JavaScript, C# plugins, and Azure Functions. She focuses on understanding how people work today and translating complex business processes into practical, scalable digital solutions.


Share Story :

SEARCH BLOGS :

FOLLOW CLOUDFRONTS BLOG :


Categories

Secured By miniOrange