Fabric has a REST API — your pipelines probably aren't using it yet
Most teams interacting with Microsoft Fabric programmatically are still clicking through the portal or using the Fabric notebooks UI. That's a problem once you need to trigger notebook runs from an external scheduler, create items across workspaces in a CI/CD flow, or cancel stalled jobs without logging into the browser.
The Fabric REST API (api.fabric.microsoft.com/v1) exposes operations that cover the full lifecycle: creating and deleting items, triggering on-demand job runs via the Job Scheduler endpoint, managing schedules (up to 20 per item), listing job instances, and bulk-importing item definitions. The API follows Azure conventions — bearer token auth, JSON payloads, and long-running operations that return 202 Accepted with a Location header you poll.
Azure Functions sits naturally in front of this API as a lightweight middleware layer. An HTTP-triggered function can accept a webhook from Azure DevOps, a timer-triggered function can kick off nightly Fabric notebook runs, and an Event Grid-triggered function can react to storage events by refreshing downstream Fabric artifacts. The pattern is straightforward: acquire a token, call the endpoint, handle the response.
But 'straightforward' breaks down in three places. Token acquisition requires the right scope and a service principal that Fabric actually recognizes. Long-running operations need polling logic that respects Retry-After headers. And consumption plan functions have a 5-minute default timeout — which is shorter than many Fabric operations take to complete. Each of these deserves its own section, because each produces a different class of silent failure.
Service principal tokens fail silently when the audience is wrong
The Fabric REST API requires an OAuth 2.0 bearer token with a specific audience. The scope you request from Azure AD (now Entra ID) must be https://api.fabric.microsoft.com/.default. Not the Power BI scope (https://analysis.windows.net/powerbi/api/.default), not the Azure management scope (https://management.azure.com/.default). Each of those will return a valid-looking JWT — and Fabric will reject it with a 401 or, worse, a 403 that tells you nothing useful.
In your Azure Function, token acquisition with Azure.Identity looks like this:
var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
var token = await credential.GetTokenAsync(
new TokenRequestContext(new[] { "https://api.fabric.microsoft.com/.default" }));For Python functions using azure-identity:
from azure.identity import ClientSecretCredential
credential = ClientSecretCredential(tenant_id, client_id, client_secret)
token = credential.get_token("https://api.fabric.microsoft.com/.default")Before any of this works, the service principal needs to be granted access in the Fabric admin portal. Navigate to Admin Portal → Tenant settings → Developer settings → Service principals can use Fabric APIs. Enable it and scope it to a security group that includes your app registration's service principal. Skip this step and every API call returns 401 — with no indication that it's a tenant configuration issue rather than a credential problem.
The app registration also needs the Fabric Service API permission (Application type) added in Entra ID → App registrations → API permissions. Without it, the token is issued but lacks the claims Fabric expects. The 403 response body will say InsufficientPrivileges, which at least points you in the right direction. Store client secrets in Azure Key Vault and reference them via the function app's Key Vault references in application settings. Hardcoded secrets in code or local.settings.json committed to repos are a common shortcut that becomes a security incident.
Long-running operations need explicit polling — 202 is not 'done'
Several Fabric REST API operations return HTTP 202 Accepted instead of an immediate result. Triggering an on-demand job via POST /v1/workspaces/{workspaceId}/items/{itemId}/jobs/instances?jobType=RunNotebook is one. Bulk export and bulk import of item definitions are others. The 202 response includes a Location header pointing to the operation status endpoint and often a Retry-After header specifying how many seconds to wait before polling.
A naive Azure Function implementation fires the POST, gets 202, and returns success to the caller. The job may have failed 30 seconds later, but nobody checks. Building correct polling logic requires a loop:
var response = await httpClient.PostAsync(jobUrl, content);
if (response.StatusCode == HttpStatusCode.Accepted)
{
var operationUrl = response.Headers.Location.ToString();
var retryAfter = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(10);
while (true)
{
await Task.Delay(retryAfter);
var statusResponse = await httpClient.GetAsync(operationUrl);
var status = JsonDocument.Parse(await statusResponse.Content.ReadAsStringAsync());
var state = status.RootElement.GetProperty("status").GetString();
if (state == "Succeeded") break;
if (state == "Failed")
throw new Exception(status.RootElement.GetProperty("error").ToString());
}
}The Get Operation State endpoint returns a status field with values like NotStarted, Running, Succeeded, and Failed. The Get Operation Result endpoint provides the actual output once the operation completes. Ignoring the distinction between state and result is a common mistake — polling the result endpoint before the operation finishes returns 200 with an empty or partial payload, not an error.
Respecting Retry-After matters beyond politeness. Fabric applies rate limiting (HTTP 429) at the tenant level. Polling more aggressively than the header suggests increases your chance of getting throttled, which then delays not just your function but every other API consumer in the tenant. When you receive a 429, the response includes its own Retry-After header — honor it or back off exponentially.
Consumption plan timeouts kill your polling loop at 5 minutes
Azure Functions on the Consumption plan have a default execution timeout of 5 minutes. The maximum configurable timeout on Consumption is 10 minutes (set functionTimeout in host.json to 00:10:00). A Fabric notebook run that takes 12 minutes to complete will outlast your function every time.
This is the most common production failure pattern for this architecture. The function triggers the Fabric job, begins polling, and gets killed by the host before the job finishes. The function logs show no error — just an abrupt end. The Fabric job itself might succeed or fail, but the function never captures the outcome.
Three approaches address this. First, use a Durable Functions orchestration instead of a single function. Durable Functions use checkpointing, so the orchestrator can await a timer for 30 seconds, poll, and repeat — without consuming compute during the wait. The total orchestration can run for days if needed:
[FunctionName("PollFabricJob")]
public static async Task RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var operationUrl = context.GetInput<string>();
while (true)
{
var status = await context.CallActivityAsync<string>("CheckStatus", operationUrl);
if (status == "Succeeded" || status == "Failed") return;
await context.CreateTimer(
context.CurrentUtcDateTime.AddSeconds(30), CancellationToken.None);
}
}Second, move to a Premium or Dedicated (App Service) plan where the timeout extends to 30 minutes or unlimited, respectively. This costs more but eliminates the architectural complexity of Durable Functions.
Third — and this is the pattern that scales best — decouple triggering from monitoring. Let the Azure Function trigger the Fabric job and immediately return the operation URL. A separate monitoring system checks whether the job completed within its expected window. If it didn't, you need to know. This is where MetricSign provides concrete value: it monitors Fabric pipeline execution end-to-end and alerts on runs that exceed expected duration or fail silently, with root cause context that includes the specific job instance ID and failure reason — details your function never captured because it timed out.
Error responses from Fabric follow a pattern — learn it once
Fabric REST API errors return a consistent JSON structure with an error object containing code and message fields. Some include an innerError with additional detail. The most frequent codes when calling from Azure Functions:
ItemNotFound (404) usually means the workspace ID or item ID is wrong, or the service principal lacks access to that specific workspace. Fabric doesn't distinguish between 'does not exist' and 'you cannot see it' — both return 404. This is a security measure, but it makes debugging harder. Double-check that the service principal is a workspace member with at least Contributor role.
Unauthorized (401) almost always traces back to the token scope issue described earlier, an expired token, or the tenant setting not enabling service principal access. If your function runs on a timer schedule, token caching can cause 401s when the cached token expires. Use GetTokenAsync with forceRefresh or let the Azure.Identity library handle token lifecycle — don't cache tokens manually.
TooManyRequests (429) is Fabric's rate limit response. The API enforces per-tenant throttling. If multiple Azure Functions, Power BI refreshes, and manual API calls all hit the same tenant simultaneously, 429s cascade. Implement exponential backoff with jitter, not fixed retry intervals. The Retry-After header value is in seconds.
InvalidItemType (400) occurs when you pass a jobType parameter that doesn't match the item type. Running jobType=RunNotebook against a Lakehouse item, for example. The Job Scheduler endpoint validates that the job type is compatible with the target item.
Log every API response — status code, headers, and body — in Application Insights. When a Fabric operation fails at 2 AM, the difference between 'something failed' and 'POST to /jobs/instances returned 429 with Retry-After: 60 at 02:14:33 UTC' is the difference between a 5-minute fix and an hour of guesswork.
Wire it together: a timer-triggered function that runs a Fabric notebook nightly
Here's the practical end-to-end pattern. A timer-triggered Azure Function acquires a service principal token, triggers a Fabric notebook run via the Job Scheduler API, and logs the operation URL for external monitoring.
[FunctionName("NightlyFabricNotebook")]
public static async Task Run(
[TimerTrigger("0 0 2 * * *")] TimerInfo timer,
ILogger log)
{
var credential = new ClientSecretCredential(
Environment.GetEnvironmentVariable("FABRIC_TENANT_ID"),
Environment.GetEnvironmentVariable("FABRIC_CLIENT_ID"),
Environment.GetEnvironmentVariable("FABRIC_CLIENT_SECRET"));
var token = await credential.GetTokenAsync(
new TokenRequestContext(new[] { "https://api.fabric.microsoft.com/.default" }));
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token.Token);
var workspaceId = Environment.GetEnvironmentVariable("FABRIC_WORKSPACE_ID");
var itemId = Environment.GetEnvironmentVariable("FABRIC_NOTEBOOK_ID");
var url = $"https://api.fabric.microsoft.com/v1/workspaces/{workspaceId}" +
$"/items/{itemId}/jobs/instances?jobType=RunNotebook";
var response = await client.PostAsync(url, null);
log.LogInformation("Fabric job trigger: {StatusCode}", response.StatusCode);
log.LogInformation("Operation URL: {Location}",
response.Headers.Location?.ToString() ?? "none");
if (response.StatusCode != HttpStatusCode.Accepted)
{
var body = await response.Content.ReadAsStringAsync();
log.LogError("Fabric API error: {Body}", body);
}
}The function triggers at 2 AM UTC. It doesn't poll — it fires and logs. This keeps execution well within the 5-minute consumption plan timeout. Configuration values come from environment variables backed by Key Vault references: @Microsoft.KeyVault(VaultName=myvault;SecretName=fabric-client-secret) in the function app's application settings.
For the CRON expression, note that Azure Functions uses NCrontab with six fields (second minute hour day month day-of-week), not the five-field format from Unix cron. Writing 0 2 instead of 0 0 2 triggers at second 0, minute 2 — every hour, not once nightly. This is a common mistake that generates dozens of unexpected Fabric job runs before anyone notices.