Power Automate: triggerOutputs function

Power Automate: triggerOutputs function

by: Manuel 13 min read 0 comments Save

Every Flow starts with a trigger, and that trigger hands you a package of information. The "triggerOutputs" function is how you open the whole package. Not just the contents, the whole thing, including the shipping label. It returns the complete output object of the trigger, so anything the trigger knew at the moment it fired is reachable from that one call.

Most of the time you only want the contents, which is why the "triggerBody" function exists and why the designer reaches for it by default. But the label matters more often than you'd think. The email address of the person who pressed the button, the request headers, the content type of the payload, none of that lives in the body. It lives one level up, and "triggerOutputs" is the only way to get there. Let's look at how it works, and at the handful of places where it quietly behaves differently from what you'd expect.

Where to find it?

You can use the function anywhere an expression is supported in Power Automate. In practice it turns up most often inside a Compose action, an Initialize variable action, a Condition action, or in a trigger condition, which is the one place where dynamic content isn't offered at all and an expression is your only option.

Don't confuse it with "triggerBody"

The "triggerBody" function returns only the body part of the trigger output. It is exactly equivalent to triggerOutputs()?['body']. If the field you want sits inside the body, either function reaches it, but the path you write is different. Mixing up the two paths is the most common reason these expressions come back empty.

Usage

The function takes no arguments. You call it, and it hands back an object:

triggerOutputs()
Parameter Required Type Description
(none) n/a n/a The function accepts no arguments. Navigation happens after the call, using bracket notation
Return value Type Description
trigger-output Object The complete output from the trigger

Microsoft's reference describes "triggerOutputs" as shorthand for trigger().outputs, so the dot notation equivalent of a body field is triggerOutputs().body.<property>. Watch out for triggerOutputs().outputs.body, which repeats a level the shorthand already covers and comes back null. The entry lists no parameters at all, so if you've seen an optional property name mentioned somewhere, that belongs to the separate "trigger" function rather than to this one. In practice you'll navigate with bracket notation instead, because dots break the moment a property name contains a hyphen, and most header names do.

What the object actually contains

The exact shape depends on the trigger, but for most connector triggers we get something along these lines:

{
  "headers": {
    "Content-Type": "application/json; charset=utf-8"
  },
  "statusCode": 200,
  "body": {
    "ID": 42,
    "Title": "Quarterly budget review",
    "Status": { "Value": "Pending" }
  }
}

The body is the payload you already know from dynamic content. The headers are the part that only "triggerOutputs" can reach.

There are two ways to reach a field inside the body, and both work:

triggerOutputs()?['body/Title']
triggerOutputs()?['body']?['Title']

The first one is the slash path notation, and it's what the designer generates when you pick dynamic content. The second is plain nested navigation. Let's break the first one down:

  • triggerOutputs() returns the whole trigger output object.
  • ?['body/Title'] walks the path body then Title in one step. The ? is the safe navigation operator, so a missing property returns null instead of taking the run down with it.

Always include that ?. Without it, a field that happens to be empty on one run breaks the whole Flow.

This is the reason the function exists. Headers sit as a sibling of the body, so there is no body/ in the path:

triggerOutputs()?['headers']?['x-ms-user-email']

On a Manually trigger a Flow trigger, that returns the email address of whoever pressed the button. Connector triggers carry a thinner header set, so check the run history before you count on any particular header being there.

The hyphens in header names are what rule out dot notation, but they're just ordinary characters once they're inside quoted brackets. The nested form above and the slash path, triggerOutputs()?['headers/x-ms-user-email'], both resolve. The nested form is the one worth reaching for, because it keeps each segment visible as its own step.

Using it in a trigger condition

Trigger conditions are typed by hand and must return a boolean, so they must start with @:

@equals(triggerOutputs()?['body/Status/Value'], 'Approved')

Let's break it down:

  • triggerOutputs() grabs the trigger's full output.
  • ?['body/Status/Value'] reaches into the body, then the Status choice column, then its Value. Choice columns are objects, not strings, which is why the path doesn't stop at Status.
  • The "equals" function compares it and returns true or false, which is what the trigger condition needs.

Real-world examples

Emailing the person who started the Flow

Maria builds a button Flow that files a request. The confirmation should go back to whoever pressed the button, not to a hard-coded address. The body of a manual trigger has no idea who that is, but the headers do:

triggerOutputs()?['headers']?['x-ms-user-email']

Drop that straight into the To field of the email action and the Flow works for the whole team without a single edit.

Skipping runs you don't care about

A SharePoint When an item is created trigger fires on every new item, and you're paying attention to a fraction of them. A trigger condition throws the rest away before the run is even created:

@and(equals(triggerOutputs()?['body/Department'], 'Finance'), greater(triggerOutputs()?['body/Amount'], 1000))

The "and" function joins the two checks, and only items matching both start a run. Everything else is discarded without consuming a run.

Reading the raw request on a webhook

A When an HTTP request is received trigger receives calls from a system you don't control. You want the payload, but you also want to know what sent it:

triggerOutputs()?['headers']?['User-Agent']

Log that next to the body, and the next time a caller starts sending malformed data, you know which one to go ask about.

Handling a field that may not exist

João's Forms When a new response is submitted trigger has an optional comments question. Rather than branching, give it a fallback with the "coalesce" function:

coalesce(triggerOutputs()?['body/Comments'], 'No comments provided')

The safe navigation returns null when the question was skipped, and "coalesce" swaps in the fallback.

Non-intuitive behaviors

Here are the behaviors that catch people off guard with the "triggerOutputs" function.

It is not a synonym for "triggerBody", even though it looks like one

triggerBody() and triggerOutputs()?['body'] return the identical object. That equivalence is real, and it's why so many people treat the two functions as interchangeable. They aren't. The "triggerBody" function can never reach the headers, because the headers are not inside the body. The moment you need the caller's email or the content type, "triggerBody" has nothing to offer and you have to move up a level.

The reverse mistake costs more. Writing triggerOutputs()?['Title'] looks perfectly reasonable and returns null every single time, because Title sits inside the body and you skipped it. There's no error, just an empty value. Check the run history for whether the field sits inside the body before you write the path.

The slash notation is a path, not a property name

?['body/Title'] looks like it's asking for a property literally named body/Title, and that's not what happens. The expression engine splits it on the slash and walks the path. This is convenient right up until a real property name contains a slash, at which point the parser splits it anyway and hands you null. Nested bracket navigation, ?['body']?['Title'], sidesteps the ambiguity entirely.

Every trigger has a different shape

There is no universal schema. A SharePoint trigger, an Outlook trigger, and a manual trigger produce three completely different objects, and even the headers differ. Manual triggers and HTTP triggers carry the richest header set, while some connector triggers carry almost nothing beyond a content type. An expression that works perfectly on one trigger returns null on another with no complaint.

The fix takes thirty seconds. Run the Flow once, open the run history, expand the trigger, and read the raw outputs. Write your paths from what you see rather than from what you assume.

The ? protects the step it sits on, not the whole chain

Safe navigation stops a missing property from throwing, but it only guards the hop it's attached to. In triggerOutputs()?['body']?['Status']?['Value'], each ? covers its own hop, which is why the chain survives a missing Status. Write triggerOutputs()?['body']['Status']['Value'] and the unguarded hops will throw the moment Status isn't there. Put the ? on every bracket and the chain stays safe all the way down.

Limitations

It only ever sees the trigger

The name is literal. No matter where in the Flow you call it, "triggerOutputs" returns the trigger's output and nothing else. For the output of an action further down, you want the "outputs" function instead, and for details about the Flow itself, the "workflow" function.

It returns an object, not a string

The result is a JSON object, so you cannot concatenate it into a message or compare it to text directly. Run it through the "string" function when you want to log the whole thing, which is a genuinely useful debugging move:

string(triggerOutputs())

Nothing validates your paths

The expression editor will accept ?['body/Titel'] without a murmur. Property paths are resolved at runtime against whatever the trigger actually sent, so a typo is not a build error, it's a null. If you want the schema enforced, run the body through a Parse JSON action and let it fail loudly.

Trigger conditions get no help at all

Inside a trigger condition there is no dynamic content picker and no test button. You type the expression blind, save it, and find out whether it works only when the Flow runs again. A condition that's silently wrong looks exactly like a Flow with nothing to do. Build the path in a Compose action first, confirm it resolves in a test run, then move the finished expression into the trigger condition.

Expression size limits still apply

As with every Power Automate expression, you have a ceiling of 8,192 characters. Long paths nested inside the "if" function eat into that faster than you'd expect, so break the work across Compose actions.

Troubleshooting Common Errors

The expression returns null and no error

Cause: Almost always a wrong path. Either you forgot body/ on a field that lives in the body, or you added it on something that doesn't, like a header. A typo in the property name produces the same silent null, since paths resolve at runtime.

Solution: Open the run history, expand the trigger, and read the raw outputs. Copy the property name from there rather than typing it.

triggerOutputs()?['body/Title']

InvalidTemplate, unable to process template language expressions

Cause: A property in the chain doesn't exist and you navigated to it without the ?. This bites hardest on optional fields and on headers, which vary from trigger to trigger.

Solution: Put ? in front of every bracket, and wrap anything genuinely optional in the "coalesce" function.

coalesce(triggerOutputs()?['headers']?['x-ms-user-email'], 'unknown')

The trigger condition saves fine but the Flow never runs

Cause: The condition evaluates to false on every event, or it isn't a boolean at all. A missing @ prefix, a path that returns null, or comparing a choice column without /Value all produce a Flow that sits there doing nothing.

Solution: Remove the condition, let the Flow run once, and read the trigger outputs from the history. Rebuild the path from the real payload, then put the condition back.

@equals(triggerOutputs()?['body/Status/Value'], 'Approved')

The value comes back as an object instead of text

Cause: You stopped one level short. Choice columns, person columns, and lookup columns are objects, so triggerOutputs()?['body/Status'] gives you the container rather than the text inside it.

Solution: Go one level deeper. Choice columns want /Value, person columns usually want /DisplayName or /Email.

triggerOutputs()?['body/Author/DisplayName']

A header exists in one Flow but not in another

Cause: Header sets are decided by the trigger, not by Power Automate. x-ms-user-email is populated on a manual trigger and simply absent on most connector triggers.

Solution: Check the run history for that specific trigger before relying on a header. When the value has to be there, get it from the body or from a dedicated action rather than from a header that may not show up.

Recommendations

Here are some things to keep in mind when using the "triggerOutputs" function.

Read the run history before writing the path

One test run tells you more than any amount of guessing. Trigger the Flow, expand the trigger in the run history, and copy the property names out of the raw output. Every silent null this function ever hands you starts with a path someone assumed.

Reach for "triggerBody" when the body is all you need

If the field lives in the body, the "triggerBody" function says so in the name and keeps the path shorter. Save "triggerOutputs" for when you genuinely need the headers or the envelope, and the next person reading the Flow will know from the function name alone that something outside the body is in play.

Put the ? on every bracket, always

There is no scenario where the safe navigation operator costs you anything, and there are plenty where leaving it out costs you a failed run. Make it a reflex.

Compose it once when you use it more than once

If three actions all reach into the trigger output, drop the value into a Compose action or a variable and reference that. One place to look when the path is wrong beats three, and the expressions downstream become readable.

Dump the whole object when you're stuck

When a path refuses to resolve and the run history isn't helping, put string(triggerOutputs()) in a Compose action and look at everything at once. It's the fastest way to find out that the field you want is spelled differently, nested deeper, or not there at all.

Always add a comment

Adding a comment will help others understand your expression. This matters twice as much in a trigger condition, where the expression is invisible from the canvas and the only symptom of a wrong one is a Flow that never runs.

Final Thoughts

The "triggerOutputs" function is the wide-angle view of your trigger. It gives you the body you already knew about plus the headers you probably didn't, and that extra level is what makes the longer path worth it. Keep two things straight and it will never surprise you. The body sits one level down, so body/ belongs in the path, and the headers sit beside it, so body/ does not. Read the run history once, write the path from what's really there, and put a ? on every bracket.

Sources

Back to the Power Automate Function Reference

Photo by Nick Fewings on Unsplash

Comments

Spotted a mistake or have a better approach? Let me know. I read and reply to every one.

💬

No comments yet

Be the first to share your thoughts on this article!

Leave a Comment

All comments are reviewed for spam before being displayed 5000 left
Replying to