Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

View all the Fabric Data Days sessions on demand. View schedule

Reply
Yazdan
Advocate I
Advocate I

Detecting the User Who Manually Triggered a Fabric Pipeline or Notebook

Hi everyone,

 

I wanted to share a useful approach I’ve been using in Microsoft Fabric to detect which user has manually triggered a pipeline or notebook, as this can be very helpful for logging, auditing, and security purposes — especially when combined with a System Run Report or other operational dashboards.


Context

While Fabric now correctly returns @pipeline().TriggerType = "1" when a pipeline is triggered by a schedule, manual runs still require additional logic if you want to capture who initiated them.

For example, when I run a pipeline or notebook manually, I log the triggering user using the following discovery logic.


Code Example

Below is a Python snippet you can run inside a Fabric Notebook to detect who triggered the run:

 

from datetime import datetime, timedelta, date
import pytz
import json
import re
from pyspark.sql.functions import col, udf, to_timestamp, lit, expr, regexp_extract, to_date
from pyspark.sql.types import StringType
from notebookutils import mssparkutils, fs
import time
import random
import hashlib
from collections import defaultdict
from pyspark.sql import SparkSession
from sempy import fabric
from sempy.fabric import FabricRestClient
import requests
 
 
# ---------------- User discovery ----------------
def get_current_job_instance_id():
    ws_id = fabric.get_notebook_workspace_id()
    nb_id = fabric.get_artifact_id()
    client = FabricRestClient()
    resp = client.get(f"/v1/workspaces/{ws_id}/items/{nb_id}/jobs/instances").json()
    runs = resp.get("value", [])
    if not runs:
        return None, None
    active = [r for r in runs if str(r.get("status","")).lower() in {"inprogress","notstarted"}]
    pick = max(active or runs, key=lambda r: r.get("startTimeUtc",""))
    return pick.get("id"), pick

def _pluck_userish(obj):
    """
    Try common shapes:
      {createdBy: {displayName, userPrincipalName, id}}
      {requestedBy: {...}}, {submittedBy: {...}}, {initiatedBy: {...}}
      {owner: {...}}, {principal: {...}}
    """
    if not isinstance(obj, dict):
        return None
    candidates = ["createdBy", "requestedBy", "submittedBy", "initiatedBy", "owner", "principal"]
    for k in candidates:
        if k in obj and isinstance(obj[k], dict):
            u = obj[k]
            return {
                "displayName": u.get("displayName"),
                "userPrincipalName": u.get("userPrincipalName") or u.get("upn"),
                "id": u.get("id") or u.get("objectId")
            }
    return None

def find_user_from_job(details):
    # check top-level common fields
    user = _pluck_userish(details)
    if user:
        return user
    # scan nested dicts one level down
    for v in details.values():
        if isinstance(v, dict):
            user = _pluck_userish(v)
            if user:
                return user
    return None

def get_current_user_via_graph():
    """
    Resolve the current interactive user via Microsoft Graph /me.
    Requires that the Fabric runtime can mint a token for Graph.
    """
    try:
        from notebookutils import mssparkutils  # available in Fabric runtime
        token = mssparkutils.credentials.getToken("https://graph.microsoft.com/")
        if not token:
            return None
        headers = {"Authorization": f"Bearer {token}"}
        r = requests.get("https://graph.microsoft.com/v1.0/me?$select=displayName,userPrincipalName,id", headers=headers, timeout=10)
        if r.status_code == 200:
            me = r.json()
            return {
                "displayName": me.get("displayName"),
                "userPrincipalName": me.get("userPrincipalName"),
                "id": me.get("id"),
                "_source": "graph"
            }
        else:
            print("Graph /me call failed:", r.status_code, r.text[:300])
    except Exception as e:
        print("Graph lookup failed:", e)
    return None

def get_user_from_runtime_context():
    """
    Fallback for manual runs: read runner from Fabric runtime context.
    Uses keys observed in your tenant: userName (display) and userId (object id).
    """
    try:
        from notebookutils import mssparkutils
        ctx = mssparkutils.runtime.context or {}
        display_name = ctx.get("userName")
        object_id    = ctx.get("userId")
        if display_name or object_id:
            return {"displayName": display_name, "userPrincipalName": None, "id": object_id, "_source": "runtime_context"}
    except Exception:
        pass
    return None

# ---------------- Run & print ----------------
job_instance_id, details = get_current_job_instance_id()
print("Job Instance ID:", job_instance_id)

run_started_by_name = None  # capture for logging later
if details:
    invoke_type = details.get("invokeType")
    print("InvokeType:", invoke_type)

    # -------- Try to resolve the user who ran it --------
    user = find_user_from_job(details)
    if user and (user.get("displayName") or user.get("userPrincipalName")):
        run_started_by_name = (user.get('displayName') or user.get('userPrincipalName'))
        print("Run started by (from job):",
              f"{user.get('displayName') or ''}".strip(),
              f"({user.get('userPrincipalName')})" if user.get("userPrincipalName") else "")
    else:
        # Fallback 1: runtime context (works in your tenant)
        user = get_user_from_runtime_context()
        if user and (user.get("displayName") or user.get("userPrincipalName")):
            run_started_by_name = (user.get('displayName') or user.get('userPrincipalName'))
            print("Run started by (from context):",
                  f"{user.get('displayName') or ''}".strip(),
                  f"({user.get('userPrincipalName')})" if user.get("userPrincipalName") else "")
        else:
            # Fallback 2: ask Graph who is running this notebook interactively
            user = get_current_user_via_graph()
            if user:
                run_started_by_name = (user.get('displayName') or user.get('userPrincipalName'))
                print("Run started by (from Graph):",
                      f"{user.get('displayName') or ''}".strip(),
                      f"({user.get('userPrincipalName')})" if user.get("userPrincipalName") else "")
            else:
                print("Run started by: Unknown (no user info in job metadata, runtime context, or Graph)")

######################################################################################
# ----------- Prepare Log Parameters (safe) -----------
if trigger_type.lower() == "pipelineactivity":
    trigger_type = "Orchestrator Pipeline"
    trigger_name = trigger_name
elif trigger_type.lower() == "1":
    trigger_type = "Scheduled"
    trigger_name = "Scheduling Module"
elif trigger_type.lower() == "manual":
    trigger_type = "Manual"
    trigger_name = run_started_by_name
else:
    trigger_type = trigger_type
    trigger_name = "UNKNOWN"

 

This code safely checks:

  • The job instance metadata

  • The Fabric runtime context

  • And as a fallback, Microsoft Graph /me

    It returns the display name or UPN of the user who manually triggered the run.


    Trigger Type Mapping

    if trigger_type.lower() == "pipelineactivity":
        trigger_type = "Orchestrator Pipeline"
        trigger_name = trigger_name
    elif trigger_type.lower() == "1":
        trigger_type = "Scheduled"
        trigger_name = "Scheduling Module"
    elif trigger_type.lower() == "manual":
        trigger_type = "Manual"
        trigger_name = run_started_by_name
    else:
        trigger_type = trigger_type
        trigger_name = "UNKNOWN"

     

    This logic helps ensure your logs clearly differentiate between manual, scheduled, and orchestrated pipeline runs — with proper user tracking for manual triggers.


    Benefit

    By logging this user information into your System Run Report or operational tables, you can generate insights like:

    • Who manually executed pipelines or notebooks

    • When and how often manual interventions occurred

    • Scheduled vs manual run ratios

    • Security auditing for sensitive pipelines


      Update

      Recently, I noticed that Fabric has fixed the previous bug — now, when triggered by a schedule, the value of @pipeline().TriggerType correctly returns "1" 🎉.

      So with this fix and the manual user detection logic above, we can now fully distinguish all run types dynamically at runtime.


      Hope this helps others who want more robust runtime tracking and reporting in Fabric!

       

      Cheers,

       

       

1 REPLY 1
v-saisrao-msft
Community Support
Community Support

Hi @Yazdan,

Thanks for sharing your insights on detecting which user manually triggered a pipeline. Definitely consider turning this into a blog post so others can benefit from your experience more easily

Power BI Community Blog - Microsoft Fabric Community

We also appreciate you sharing this with the community

 

Thank you.

Helpful resources

Announcements
November Fabric Update Carousel

Fabric Monthly Update - November 2025

Check out the November 2025 Fabric update to learn about new features.

Fabric Data Days Carousel

Fabric Data Days

Advance your Data & AI career with 50 days of live learning, contests, hands-on challenges, study groups & certifications and more!

FabCon Atlanta 2026 carousel

FabCon Atlanta 2026

Join us at FabCon Atlanta, March 16-20, for the ultimate Fabric, Power BI, AI and SQL community-led event. Save $200 with code FABCOMM.

Top Kudoed Authors