This is the set of questions that shows up in any interview aimed at a developer role — the plugin pipeline, sync vs. async design decisions, debugging habits, and the practical differences between the ways you can query Dataverse. Each answer opens with a quick, everyday comparison to build the intuition, then follows with the specific mechanics an interviewer is actually checking for. Interviewers here are usually less interested in whether you've memorized stage numbers and more interested in whether you can reason about why a plugin should be synchronous or asynchronous, or why a query is timing out — that's the angle these answers are written from.

Plugin Pipeline & Execution

Think of the plugin pipeline like the checkpoints between an airport entrance and your seat on the plane — there's an ID check before you're let past the counter, a bag scan right before boarding, and a headcount once everyone's seated. Technically, the pipeline is the sequence of stages Dataverse runs a message like Create or Update through: pre-validation, the platform's core operation, pre-operation, the actual database write, and post-operation. IPluginExecutionContext is what a plugin receives at any of those stages, exposing the InputParameters and OutputParameters for the message, the PreEntityImages and PostEntityImages you registered, the Depth and IsExecutingOffline properties used for loop protection, and the identity of the calling user.

Staying with the airport picture — pre-validation is the ID check before you're even let into the security line, pre-operation is the bag scan right before boarding, and post-operation is the crew doing a headcount once everyone's already seated. Pre-validation (stage 10) runs before the main system operation, and even before the database transaction begins, so it's the right place for logic that should run even if the platform later rolls back, like blocking an operation outright. Pre-operation (stage 20) runs inside the transaction, just before the core platform logic executes, and can still modify the target record before it's written. Post-operation (stage 40) runs after the write has completed and the record exists, or was updated, in the database — the stage for logic that depends on the final state, like triggering related updates.

Synchronous is like standing at the bank counter and waiting while the teller finishes your transaction before you walk away; asynchronous is like dropping a form in a drop box and getting a text once it's been processed. A synchronous plugin runs in the same transaction as the triggering operation and blocks the user's request until it finishes — use it when the business logic must complete, or the whole operation must roll back, before the user gets a response, such as validation that should stop a save. An asynchronous plugin is queued onto the system job infrastructure and runs after the response has already returned to the user, which fits anything that isn't time-critical to the immediate save, like sending a notification, calling a slower external API, or heavy secondary processing.

You can't drop off a form and get a text back later if the whole point of the form was to decide, right now, whether you're even allowed in the building. No — pre-validation and pre-operation stages only support synchronous execution, because they run inside, or immediately around, the core transaction, and the platform needs their result before deciding whether to proceed. Only the post-operation stage supports both synchronous and asynchronous registration, since by then the transaction has already committed and there's nothing left for an async job to block.

It's like a checkpoint line with numbered tickets — whoever holds the lower number goes first, no matter what order people actually walked up. They run in the order defined by the Rank value set on each step registration — lower rank executes first. When designing a solution with multiple plugins on the same trigger, it's worth being deliberate about rank values up front, because implicit ordering, like registration date, isn't something you can rely on, and unexpected ordering is a common source of bugs where behavior changes depending on which plugin ran first.

A pre-image is like a "before" photo taken right before something happens, and a post-image is the "after" photo taken once it's finished — useful for very different questions. A pre-image is a snapshot of the target record's attribute values before the core operation runs, and it's what you use in an Update plugin to see a field's old value, since the Target in InputParameters on an Update message only contains the attributes that actually changed. A post-image is a snapshot after the operation completes, useful in post-operation plugins that need the full, final state of the record, including system-calculated fields like a newly generated ID, rather than just the attributes the caller sent.

Plugin Configuration & Design Patterns

Unsecure configuration is like a sticky note on a filing cabinet that anyone walking past can read; secure configuration is more like a locked drawer only certain staff have the key to. Unsecure configuration is plain text, visible to anyone with access to view the plugin step's registration, and is fine for non-sensitive settings like a threshold value or a behavior flag. Secure configuration is stored separately and isn't visible through the plugin registration UI to users without System Administrator or System Customizer privileges, so it's the right place for anything sensitive, like an API key or connection detail the plugin needs.

A custom workflow activity is like a pre-built building block that a non-developer can snap into different designs through the workflow designer; a plugin is more like a structural beam wired directly into one specific part of the building. A custom workflow activity is designed to be dropped into a classic workflow or a Business Process Flow as a reusable step, configured with input/output parameters through the workflow designer — the right tool when the goal is giving non-developers a reusable building block they can wire into different processes visually. A plugin is the right tool when logic needs to run tied directly to a specific message on a specific table, especially anything that must run synchronously as part of the core save, which a workflow activity generally can't guarantee the same way.

This is like a store not letting you close an account that still has an open tab — you have to settle up first. I'd register a plugin on the pre-validation stage of the Delete message on Account, so the check runs before the platform commits to deleting anything. Inside the plugin, I'd query for related Opportunities where the status isn't Won or Lost, and if any exist, throw an InvalidPluginExecutionException with a clear message — that surfaces as a blocking error to the user, and the deletion never proceeds, rather than trying to clean up after a delete that already happened.

It's like a manager updating a project's status and that change automatically rippling down to every task under it, without anyone updating each task by hand. I'd register it post-operation on Update of the parent, filtered with a registered attribute so it only fires when the relevant status field actually changes. Inside, I'd retrieve the related child records via the Web API or QueryExpression and update them — typically as an asynchronous step, since cascading isn't usually something the user needs to wait on, and doing it async also avoids extending the parent record's save transaction with what could be a large number of child updates.

Calling an external API synchronously is like making someone wait at the counter while you phone a supplier to check stock — if that call is slow, everyone behind them waits too. A synchronous plugin calling an external service ties the user's save operation to that service's latency and reliability — if the external API is slow or down, the user's save hangs or fails for a reason that has nothing to do with Dynamics 365 CE itself. Where the call doesn't need to block the save, I'd move it to an asynchronous post-operation plugin, or hand it off to a Power Automate flow, and add explicit timeout handling and a sensible retry/failure strategy rather than letting an unhandled exception fail the entire transaction.

Plugin Debugging & Reliability

It's the platform's built-in circuit breaker — like an elevator that refuses to keep going once it senses it's stuck bouncing between the same two floors. The platform tracks a Depth value on the execution context that increments every time a plugin's own operation triggers another plugin execution recursively, and it enforces a maximum depth, by default around 8, after which it throws an exception rather than continuing. Well-written plugins also add their own explicit guard, checking context.Depth or comparing pre- and post-image values before performing an update that could re-trigger the same step, instead of relying solely on the platform's ceiling.

Rather than waiting for the circuit breaker to trip, it's better to just stop flipping the same switch that keeps triggering it in the first place. The cleanest fix is registering the plugin's step with a filtering attribute list that only includes the fields it actually cares about, so an update that only touches the field the plugin itself sets doesn't re-trigger it. As a second layer, I'd compare the new value against the pre-image before writing, and skip the update entirely if the value hasn't actually changed — that avoids both the extra database write and the re-trigger.

Plugin trace logs are like a flight data recorder — you don't need them until something goes wrong, and then they're the first thing you pull. Plugin trace logs are the first stop — writing meaningful messages via ITracingService inside the plugin, then reviewing the resulting plugin trace log records, or turning on verbose tracing at the environment level for a wider net, usually surfaces exactly where execution failed. For reproducing and stepping through logic before it's live, a local unit test harness or a profiler tool that captures a real execution context and replays it locally lets you debug without touching production data directly.

A plugin instance is like one shared cashier register used by several cashiers at once — if one cashier leaves their own notes lying in the drawer, the next person to use that register can trip over them. A single compiled plugin assembly is instantiated once per registration, and its Execute method is invoked concurrently across many unrelated transactions, potentially by multiple users at the same time. Storing request-specific data in instance fields, rather than local variables inside Execute, risks one transaction's data leaking into or being overwritten by another running concurrently — a subtle, hard-to-reproduce bug class specific to the plugin execution model.

Web API & Query Approaches

The Web API's verbs map almost exactly onto ordering at a restaurant — look at the menu, place a new order, change something on an existing order, or cancel it outright. GET retrieves records like browsing the menu, POST creates a new record like placing an order, PATCH performs a partial update to an existing record like swapping a side dish, and DELETE removes a record like cancelling the order. There's also a special use of POST with an upsert-style header, or a PATCH to a specific alternate key, for upsert scenarios, and POST is additionally used to invoke bound and unbound custom actions or functions.

$select is like asking the waiter to bring only the dishes you actually ordered instead of everything the kitchen has; $filter narrows down which orders come out at all, and $expand asks for a side dish to arrive on the same tray instead of a separate trip. $select limits the columns returned to only the ones named, $filter applies OData conditions to restrict which rows come back, and $expand pulls in related records from a navigation property in the same request instead of requiring a separate round trip. $select matters for performance because, without it, a query returns every column on the table by default — on a wide table that's unnecessary payload on every single request, and it's one of the simplest, most overlooked Web API optimizations.

These are three different languages for asking the database the same question — one you speak in code, one you write as XML, and one you send over plain HTTP. QueryExpression is a strongly typed, in-process .NET object model for building queries against the SDK — convenient inside plugins and other server-side .NET code because you build it with objects, not strings. FetchXML is an XML-based query language that both server-side and client-side tooling understand, and it supports some things QueryExpression doesn't, like aggregation queries and fetching related records across a link-entity graph in a single readable structure. Web API OData query syntax is the standard way to query from outside the .NET SDK entirely — JavaScript web resources, external integrations, or any HTTP client — using $filter, $select, and $expand instead of either of the other two.

This is one of the cases where you deliberately reach for the one language built for the job, instead of forcing a tool to do something it was never designed to say. FetchXML is generally the most straightforward path for aggregation, since it has built-in support for aggregate attributes and grouping through the aggregate and groupby attributes on a fetch element, and that FetchXML can be passed to the Web API's fetchXml query parameter if it needs to be delivered over HTTP. QueryExpression doesn't support aggregation directly, which is one of the concrete cases where reaching for FetchXML instead is the right call even from server-side .NET code.

A batch request is like handing a waiter a whole list of orders for the table at once instead of calling them back to your table for every single item. A batch request bundles multiple individual operations — a mix of creates, updates, deletes, or retrieves — into a single HTTP request using the multipart/mixed content type, so they're sent and processed together instead of as separate round trips. It's the right choice when you need to make many related calls in one logical unit of work, particularly when you also want them wrapped in a single transaction so that if one operation fails, the whole batch can be rolled back together.

It's like following the "continued on next page" note in a magazine article instead of guessing where the article picks back up. The Web API returns an @odata.nextLink value in the response when more records exist beyond the current page, and you follow that link directly for the next page rather than constructing pagination parameters yourself. You can also set a Prefer: odata.maxpagesize header to control how many records come back per page, useful for tuning between fewer round trips and smaller individual payloads depending on record size.

Integration & Authentication Concepts

It works like showing a badge at a building's front desk that was issued by a trusted authority, rather than the front desk trying to verify your identity itself. The external application registers an app in Azure AD (Microsoft Entra ID), which issues a client ID and either a client secret or certificate; the app uses those credentials to request an OAuth 2.0 access token from Azure AD, then presents that token as a bearer token on each Web API call. Dataverse validates the token against the tenant and checks the calling application, or the user it's acting on behalf of, against its own security role model before letting the request through.

An application user is like a badge issued to a vending machine rather than to a person — it doesn't go on leave, get a new phone, or need its password reset. An application user is created specifically for non-interactive, service-to-service integration — it's tied to an Azure AD app registration rather than a real person, doesn't consume a full user license the way an interactive login does, and is assigned only the security roles the integration actually needs. Using an application user instead of hardcoding a real employee's credentials into an integration is the standard, safer pattern, since it isn't affected by that person leaving the organization or having their password rotated.

The gateway is like a secure mail slot between a private building and the outside world — cloud services can reach through it to grab what they need without the whole building being thrown open to the internet. An on-premises data gateway is a bridge that lets cloud services like Power Automate, Power BI, or Power Apps reach data sources that live inside a private network, such as an on-prem SQL Server or file share, without exposing that data source directly to the internet. It matters for integration design because a cloud flow or custom connector calling an internal system has to route through the gateway, which also becomes a factor in performance and availability planning since the gateway machine is a dependency the cloud service doesn't otherwise have.