🚀 New batches open: Advanced Excel • Power BI • SQL • AI for Analytics — Book a free demo

Day 9 — The shape of real data

Most flows do not fail on logic. They fail because the data was not the shape you assumed.

Object or array?

Everything you get from a connector is one of two things, and telling them apart is most of the battle.

ObjectArray
Looks like{ "Name": "Anita" }[ {...}, {...} ]
IsOne thing with named fieldsA list of things
Read with?['Name']item() in a loop, or first()
Comes fromOne form response, one itemGet items, Get rows, an attachments list
@outputs('One')?['Name']
@length(outputs('Many'))
@first(outputs('Many'))?['Name']
@last(outputs('Many'))?['Name']
@outputs('Many')?[0]?['Name']
The mistake that creates a loop you did not want Use a field from an array and Power Automate silently wraps the whole rest of your flow in an Apply to each. If you only ever expect one row, use first() instead and keep the flow flat: first(outputs('Get items')?['value'])?['Title'].

Nested fields

Real connector output is objects inside objects. You chain the brackets.

{
  "trigger": {
    "name": "When an item is created",
    "type": "automated",
    "body": {
      "ID": 42,
      "Title": "Enrolment request",
      "Student": { "Name": "Priya Nair", "Email": "priya@example.com", "Phone": null },
      "Course":  { "Name": "Tableau", "Fee": 14000, "Trainer": { "Name": "Meera", "Id": 7 } },
      "Tags": ["urgent", "weekend"]
    }
  },
  "actions": [
    { "name": "Student name", "type": "Compose", "input": "@triggerBody()?['Student']?['Name']" },
    { "name": "Trainer name", "type": "Compose",
      "input": "@triggerBody()?['Course']?['Trainer']?['Name']" },
    { "name": "Missing phone", "type": "Compose",
      "input": "@coalesce(triggerBody()?['Student']?['Phone'], 'Not given')" },
    { "name": "Deep and missing", "type": "Compose",
      "input": "@triggerBody()?['Course']?['Syllabus']?['Url']" },
    { "name": "First tag", "type": "Compose", "input": "@first(triggerBody()?['Tags'])" },
    { "name": "Summary", "type": "Compose",
      "input": "@{triggerBody()?['Student']?['Name']} wants @{triggerBody()?['Course']?['Name']} with @{triggerBody()?['Course']?['Trainer']?['Name']}" }
  ]
}
A ? at every step "Deep and missing" reaches through a field that does not exist and still returns null rather than failing, because every hop has its question mark. Miss one and the whole chain breaks the day a field is empty.

When the data arrives as text

An HTTP response, a file, or a column somebody stuffed JSON into gives you a string that happens to look like JSON. json() turns it into something you can read fields from.

{
  "trigger": { "type": "manual",
    "body": { "Payload": "{\"Name\":\"Rahul\",\"Scores\":[8,6,9]}" } },
  "actions": [
    { "name": "As text", "type": "Compose", "input": "@triggerBody()?['Payload']" },
    { "name": "Length of the text", "type": "Compose",
      "input": "@length(triggerBody()?['Payload'])" },
    { "name": "Parsed", "type": "Compose", "input": "@json(triggerBody()?['Payload'])" },
    { "name": "Name from it", "type": "Compose",
      "input": "@json(triggerBody()?['Payload'])?['Name']" },
    { "name": "Best score", "type": "Compose",
      "input": "@max(json(triggerBody()?['Payload'])?['Scores'])" }
  ]
}

"Length of the text" gives the number of characters, because before json() it really is just a string. That is the tell: if length() returns something far too big, you are looking at text, not data.

Cleaning what people typed

Any field a human filled in will arrive with stray spaces, mixed capitals and inconsistent phone formats.

{
  "trigger": { "type": "manual",
    "body": {
      "Name": "  anita SHARMA  ",
      "Email": " Anita.Sharma@Example.COM ",
      "Phone": "+91 98765 43210",
      "Region": "north"
    } },
  "actions": [
    { "name": "Clean name", "type": "Compose",
      "input": "@trim(triggerBody()?['Name'])" },
    { "name": "Clean email", "type": "Compose",
      "input": "@toLower(trim(triggerBody()?['Email']))" },
    { "name": "Digits only", "type": "Compose",
      "input": "@replace(replace(replace(triggerBody()?['Phone'], '+91', ''), ' ', ''), '-', '')" },
    { "name": "Region matched", "type": "Compose",
      "input": "@equals(toLower(trim(triggerBody()?['Region'])), 'north')" },
    { "name": "Proper case", "type": "Compose",
      "input": "@concat(toUpper(substring(trim(triggerBody()?['Name']), 0, 1)), toLower(substring(trim(triggerBody()?['Name']), 1)))" }
  ]
}
Clean at the front door Do the trimming and lower-casing in the first two or three actions, put the results in Compose actions, and use those everywhere downstream. Cleaning the same field in eight different expressions is how one of them ends up different from the others.

Multi-select answers

A Forms checkbox question does not give you an array. It gives you one string with semicolons in it, and everybody trips over this once.

{
  "trigger": { "type": "manual",
    "body": { "Name": "Vikram", "Interests": "Excel;Power BI;SQL" } },
  "actions": [
    { "name": "Raw", "type": "Compose", "input": "@triggerBody()?['Interests']" },
    { "name": "Length of the raw text", "type": "Compose",
      "input": "@length(triggerBody()?['Interests'])" },
    { "name": "As an array", "type": "Compose",
      "input": "@split(triggerBody()?['Interests'], ';')" },
    { "name": "How many chosen", "type": "Compose",
      "input": "@length(split(triggerBody()?['Interests'], ';'))" },
    { "name": "Wants Power BI", "type": "Compose",
      "input": "@contains(split(triggerBody()?['Interests'], ';'), 'Power BI')" },
    { "name": "Each interest", "type": "ApplyToEach",
      "from": "@split(triggerBody()?['Interests'], ';')",
      "actions": [
        { "name": "Note", "type": "Compose", "input": "Interested in @{item()}" }
      ] }
  ]
}

The raw text is 18 characters. Split on the semicolon and it becomes three items you can loop, count and test.

Try these yourself

  1. Reach a name three levels deep in a nested object, safely.
  2. Parse a JSON string out of a field and read two values from it.
  3. Clean a name and an email that arrive with spaces and mixed capitals.
  4. Turn a semicolon-separated answer into an array and count the choices.
  5. Explain how to tell whether a connector gave you an object or an array.