Using Power BI to retrieve boards using Cursor-based pagination

Found the proper way to look a single query dynamically. With the help of Parker from BI Elite.
NJM Banana (:15) (youtube.com)

This is my primary function doing the loop, based on Parker’s video.

(page as number, optional continuationCursor as text, optional data as list) =>
let
//need a dynamic pageNum MAX in case data grows substantially in the future
itemsCount = Web.Contents(“https://api.monday.com/v2”,
[
Headers=[
#“Method”=“POST”,
#“Content-Type”=“application/json”,
#“API-Version”=#“UseApiVersion”,
#“Authorization”=“Bearer " & #“AuthKey”
],
Content=Text.ToBinary(”{““query””: ““query { boards(ids: " & BoardNbr_SalesPipeline & “) { items_count } }””}”)
]),
itemsCountData = Json.Document(itemsCount)[data][boards]{0}[items_count],
maxPageNum = Int64.From(((itemsCountData / 100) + 10)),

firstPage = Text.ToBinary("{""query"": ""query { boards(ids: " & BoardNbr_SalesPipeline & ") { items_page (limit: 100) { cursor, items { name, updated_at, group { title }, columns: column_values { column { title }, text } } } } }""}"),
nextPage = Text.ToBinary("{""query"": ""query { next_items_page (limit: 100, cursor: \""" & continuationCursor & "\"") { cursor, items { name, updated_at, group { title }, columns: column_values { column { title }, text } } } }""}"),

Source = Json.Document(Web.Contents("https://api.monday.com/v2",
[   
    Headers=[
        #"Method"="POST",
        #"Content-Type"="application/json",
        #"API-Version"=#"UseApiVersion",
        #"Authorization"="Bearer " & #"AuthKey"
    ],
Content=
    if page = 1
    then firstPage
    else nextPage
])),
nextCursor = 
    if page = 1
    then (Source)[data][boards]{0}[items_page][cursor]
    else (Source)[data][next_items_page][cursor],
currentData =
    if page = 1
    then (Source)[data][boards]{0}[items_page][items] //this only works on page 1 which has [boards] as part of the results response
    else (Source)[data][next_items_page][items], //this only works on followup pages using the cursor and the [next_items_page]
                                                //but once these results reach this state, the file list of records are identical and can be appended without an issue
appendedData =
    if currentData is null and data is null then {}
    else if data is null then List.Combine({{}, currentData})   //I am not 100% certain the purpose of having this line and the next. I would assume if a null result returned in the data, this 1st line would take effect and the next line would never occur
    else if data is null then List.Combine({data, {}})
    else List.Combine({data, currentData}),
pageNum = page + 1,
output =
    if nextCursor is null or maxPageNum > 100   //the cursor should come back null when the final page is reached and there are no further pages of results to return. This will still work on the FirstPage call also because that will return the Cursor if there is a Page 2 available.
                                                //a Max Page needs used to make sure it doesn't get stuck in an endless loop
    then appendedData //I don't believe the next line was needed for our purpose, instead, I'm just doing "then appendedData" here
    //if appendedData is null then {1,2,3} else appendedData    //this line came from the example of this API Loop found on BI Elite YouTube channel
    else @GetSalesPipelineCursorPagination(pageNum,nextCursor,appendedData)

in
output

Then this is my actual table running that function on it. It isn’t 100% complete yet because I need to expand the Records and Lists yet, but its 99% there by being successful so far.

let
Source = List.Numbers(1,1),
#“Converted to Table” = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#“Starting Page” = Table.TransformColumnTypes(Table.RenameColumns(#“Converted to Table”,{{“Column1”, “StartingPage”}}),{{“StartingPage”, Int64.Type}}),
#“Invoked Custom Function” = Table.AddColumn(#“Starting Page”, “SalesPipelineApiResults”, each GetSalesPipelineCursorPagination([StartingPage], null, null)),
#“Expanded ListToRows” = Table.ExpandListColumn(#“Invoked Custom Function”, “SalesPipelineApiResults”),
#“Expanded RecordsToColumns” = Table.ExpandRecordColumn(#“Expanded ListToRows”, “SalesPipelineApiResults”, {“name”, “updated_at”, “group”, “columns”}, {“name”, “updated_at”, “group”, “columns”})
in
#“Expanded RecordsToColumns”

There was only 1 final step to open up all of the records.

let
Source = List.Numbers(1,1),
#“Converted to Table” = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#“Starting Page” = Table.TransformColumnTypes(Table.RenameColumns(#“Converted to Table”,{{“Column1”, “StartingPage”}}),{{“StartingPage”, Int64.Type}}),
#“Invoked Custom Function” = Table.AddColumn(#“Starting Page”, “SalesPipelineApiResults”, each GetSalesPipelineCursorPagination([StartingPage], null, null)),
#“Expanded ListToRows” = Table.ExpandListColumn(#“Invoked Custom Function”, “SalesPipelineApiResults”),
#“Expanded RecordsToColumns” = Table.ExpandRecordColumn(#“Expanded ListToRows”, “SalesPipelineApiResults”, {“name”, “updated_at”, “group”, “columns”}, {“Title”, “UpdatedAt”, “Group”, “Columns”}),
#“Expanding RecordsAndList” = Table.FromRecords(Table.TransformRows(#“Expanded RecordsToColumns”, each
List.Accumulate([Columns], [
Title = [Title],
UpdateDate = [UpdatedAt],
Group = [Group][title]
], (state, current) => Record.AddField(state, current[column][title], current[text]) )
)),

Here is my completed looping function, I added in the dynamic max page number to use in the loop exit options of the “output” variable at the end.

(pageNbr as number, optional continuationCursor as text, optional data as list) =>
let
//need a dynamic pageNum MAX in case data grows substantially in the future
itemsCount = Web.Contents(“https://api.monday.com/v2”,
[
Headers=[
#“Method”=“POST”,
#“Content-Type”=“application/json”,
#“API-Version”=#“UseApiVersion”,
#“Authorization”=“Bearer " & #“AuthKey”
],
Content=Text.ToBinary(”{““query””: ““query { boards(ids: " & BoardNbr_SalesPipeline & “) { items_count } }””}”)
]),
itemsCountData = Json.Document(itemsCount)[data][boards]{0}[items_count],
maxPageNum = Int64.From(((itemsCountData / 100) + 10)),

firstPage = Text.ToBinary("{""query"": ""query { boards(ids: " & BoardNbr_SalesPipeline & ") { items_page (limit: 100) { cursor, items { name, updated_at, group { title }, columns: column_values { column { title }, text } } } } }""}"),
nextPage = Text.ToBinary("{""query"": ""query { next_items_page (limit: 100, cursor: \""" & continuationCursor & "\"") { cursor, items { name, updated_at, group { title }, columns: column_values { column { title }, text } } } }""}"),

Source = Json.Document(Web.Contents("https://api.monday.com/v2",
[   
    Headers=[
        #"Method"="POST",
        #"Content-Type"="application/json",
        #"API-Version"=#"UseApiVersion",
        #"Authorization"="Bearer " & #"AuthKey"
    ],
Content=
    if pageNbr = 1
    then firstPage
    else nextPage
])),
nextCursor = 
    if pageNbr = 1
    then (Source)[data][boards]{0}[items_page][cursor]
    else (Source)[data][next_items_page][cursor],
currentData =
    if pageNbr = 1
    then (Source)[data][boards]{0}[items_page][items] //this only works on page 1 which has [boards] as part of the results response
    else (Source)[data][next_items_page][items], //this only works on followup pages using the cursor and the [next_items_page]
                                                //but once these results reach this state, the file list of records are identical and can be appended without an issue
appendedData =
    if currentData is null and data is null then {}
    else if data is null then List.Combine({{}, currentData})   //I am not 100% certain the purpose of having this line and the next. I would assume if a null result returned in the data, this 1st line would take effect and the next line would never occur
    else if data is null then List.Combine({data, {}})
    else List.Combine({data, currentData}),
nextPageNbr = pageNbr + 1,
output =
    if nextCursor is null or nextPageNbr > maxPageNum   //the cursor should come back null when the final page is reached and there are no further pages of results to return. This will still work on the FirstPage call also because that will return the Cursor if there is a Page 2 available.
                                                //a Max Page needs used to make sure it doesn't get stuck in an endless loop
    then appendedData //I don't believe the next line was needed for our purpose, instead, I'm just doing "then appendedData" here
    //if appendedData is null then {1,2,3} else appendedData    //this line came from the example of this API Loop found on BI Elite YouTube channel
    else @GetSalesPipelineCursorPagination(nextPageNbr,nextCursor,appendedData)

in
output

Hi, Howard
Changing the privacy level really solves it for the desktop version.
But when I publish it to the server (on-premise) it doesn’t work.
Is there a parameter I need to change on the server too?

Hi @davidalkoby

Are you using a Power BI Report Server on-premise? I don’t have any experience publishing to a Power BI Report Server.

Have you or do you have the ability to publish to the Power BI Service (in the Microsoft cloud)? I know that the privacy level is maintained when publishing to the Power BI Service.

Have you tried posting in the Power BI Forums/Microsoft Fabric Community? It seems like someone else is having a problem with the Reporting Server and privacy levels.

Good luck!

Hi, Howard
Thanks for your quick response.
We are using On-premise report server for various reasons.
I did see that working in with cloud i.e. report service has the ability to keep the privacy levels but as I searched through the Microsoft boards there’s no such tool for the report server.
I hoped that someone here has some experience or tweak to make it possible.

Hi @davidalkoby

I found this indicating that data privacy levels are not supported on Power BI Report Server

If you look at the comparison chart at the third last row it shows the bad news. Some possible good news is you might be able to achieve the same thing using Roles.

Unfortunately, I don’t have any experience with this or the environment to test.

Hi, Howard
Thanks for all the code you uploaded and help.
Just to let anyone who has the same problem - I solved it!!!
You should change the privacy level for the whole desktop and not just for this specific file.

Hello, Howard! How are you?

I’ve been using your query to integrate Power BI with Monday.comand it was working perfectly. However, today there was an error in updating the data and when I checked, the query had the following error:

GetBoard_1stPage’. Expression.Error: We couldn’t convert the null value to List type.

Can you help me?

Hi @arianetiffani. My guess is that you have a blank value in the first column of one of the rows of data in your board.

Hi Andrew, thank you for providing the code. I’ve been attempting to retrieve data, but it’s not functioning as anticipated. Do you happen to have the complete code where we can dynamically fetch all the data from any board on Monday.com? I have approximately 3000 records, which are gradually increasing, so I’m seeking a dynamic query to manage this. Your guidance would be greatly appreciated.

Hello @ASYADAV5,

Here is what I got to work for retrieving a complete table of multiple pages.
This picture shows you its all done with 3 parameters, 1 function that loops, and 1 table that is loaded as the final results.
image

Important notes:
-parameters were all set to be Text, even the Board Number
-the loaded table, which runs the function and brings all results together, had to do some extra steps because this Monday Board contained some fields that are Connected Boards. Due to changes for the 2023-10 API Version, those values no longer come back from the simple Values call. Now it needed to include "… on BoardRelationValue {display_value} to get those values as another column in the results. But the issue was then PowerQuery couldn’t handle that without throwing errors because some records had the 3rd column and other records didn’t if a value wasn’t present.
-technically this might work for any board. I think column/field specifics are only coming into play once results are in PowerQuery for additional ETL steps. I am not familiar enough with the Monday Querying to say nothing is specific between this and what is present on the actual Board. These may be all meta-data naming that exist for all or more Boards.
-I don’t know if this was an Us problem or a Monday problem, but when going to the 2024-04 version, we started getting some concatenated duplicates of 1 field.

Code below, this is the entire M-code of each query.

This first 1 is the Function
(pageNbr as number, optional continuationCursor as text, optional data as list) =>
let
//need a dynamic pageNum MAX in case data grows substantially in the future
itemsCount = Web.Contents(“https://api.monday.com/v2”,
[
Headers=[
#“Method”=“POST”,
#“Content-Type”=“application/json”,
#“API-Version”=#“UseApiVersion”,
#“Authorization”=“Bearer " & #“AuthKey”
],
Content=Text.ToBinary(”{““query””: ““query { boards(ids: " & BoardNbr_SalesPipeline & “) { items_count } }””}”)
]),
itemsCountData = Json.Document(itemsCount)[data][boards]{0}[items_count],
maxPageNum = Int64.From(((itemsCountData / 100) + 10)),

firstPage = Text.ToBinary("{""query"": ""query { boards(ids: " & BoardNbr_SalesPipeline & ") { items_page (limit: 100) { cursor, items { id, name, updated_at, group { title }, column_values { column { title }, text, ... on BoardRelationValue { display_value }} } } } }""}"),
nextPage = Text.ToBinary("{""query"": ""query { next_items_page (limit: 100, cursor: \""" & continuationCursor & "\"") { cursor, items { id, name, updated_at, group { title }, column_values { column { title }, text, ... on BoardRelationValue { display_value }} } } }""}"),

Source = Json.Document(Web.Contents("https://api.monday.com/v2",
[   
    Headers=[
        #"Method"="POST",
        #"Content-Type"="application/json",
        #"API-Version"=#"UseApiVersion",
        #"Authorization"="Bearer " & #"AuthKey"
    ],
Content=
    if pageNbr = 1
    then firstPage
    else nextPage
])),
nextCursor = 
    if pageNbr = 1
    then (Source)[data][boards]{0}[items_page][cursor]
    else (Source)[data][next_items_page][cursor],
currentData =
    if pageNbr = 1
    then (Source)[data][boards]{0}[items_page][items] //this only works on page 1 which has [boards] as part of the results response
    else (Source)[data][next_items_page][items], //this only works on followup pages using the cursor and the [next_items_page]
                                                //but once these results reach this state, the file list of records are identical and can be appended without an issue
appendedData =
    if currentData is null and data is null then {}
    else if data is null then List.Combine({{}, currentData})   //I am not 100% certain the purpose of having this line and the next. I would assume if a null result returned in the data, this 1st line would take effect and the next line would never occur
    else if data is null then List.Combine({data, {}})
    else List.Combine({data, currentData}),
nextPageNbr = pageNbr + 1,
output =
    if nextCursor is null or nextPageNbr > maxPageNum   //the cursor should come back null when the final page is reached and there are no further pages of results to return. This will still work on the FirstPage call also because that will return the Cursor if there is a Page 2 available.
                                                //a Max Page needs used to make sure it doesn't get stuck in an endless loop
    then appendedData //I don't believe the next line was needed for our purpose, instead, I'm just doing "then appendedData" here
    //if appendedData is null then {1,2,3} else appendedData    //this line came from the example of this API Loop found on BI Elite YouTube channel
    else @GetSalesPipelineCursorPagination(nextPageNbr,nextCursor,appendedData)

in
output

this 1 is the loaded table that runs the function:
let
Source = List.Numbers(1,1),
#“Converted to Table” = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#“First Page” = Table.TransformColumnTypes(Table.RenameColumns(#“Converted to Table”,{{“Column1”, “FirstPage”}}),{{“FirstPage”, Int64.Type}}),
#“Invoked Custom Function” = Table.AddColumn(#“First Page”, “SalesPipelineApiResults”, each GetSalesPipelineCursorPagination([FirstPage], null, null)),
#“Expanded ListToRows” = Table.ExpandListColumn(#“Invoked Custom Function”, “SalesPipelineApiResults”),
#“Expanded RecordsToColumns” = Table.ExpandRecordColumn(#“Expanded ListToRows”, “SalesPipelineApiResults”, {“id”, “name”, “updated_at”, “group”, “column_values”}, {“ItemId”, “Title”, “UpdatedAt”, “Group”, “Columns”}),
#“Expand Columns to Records” = Table.ExpandListColumn(#“Expanded RecordsToColumns”, “Columns”),
#“Expand Text Values” = Table.ExpandRecordColumn(#“Expand Columns to Records”, “Columns”, {“column”, “display_value”, “text”}, {“column”, “display_value”, “text.original”}),
#“Added Conditional Column” = Table.AddColumn(#“Expand Text Values”, “text”, each if [display_value] <> null then [display_value] else [text.original]),
#“Added Custom” = Table.AddColumn(#“Added Conditional Column”, “columns”, each Record.SelectFields([[column],[text]],{“column”,“text”})),
#“Removed Columns” = Table.SelectColumns(#“Added Custom”,{“ItemId”, “Title”, “UpdatedAt”, “Group”, “columns”}),
#“Grouped Rows” = Table.Group(#“Removed Columns”, {“ItemId”, “Title”, “UpdatedAt”, “Group”}, {{“Columns”, each _[columns] }}),
#“Expanding RecordsAndList” = Table.FromRecords(Table.TransformRows(#“Grouped Rows”, each
List.Accumulate([Columns], [
ItemId = [ItemId],
Title = [Title],
UpdateDate = [UpdatedAt],
Group = [Group][title]
], (state, current) => Record.AddField(state, current[column][title], current[text]) )
))
in
#“Expanding RecordsAndList”

2 Likes

@JustAnotherMonday Thank you very much, its really great help for fast track.

Your very welcome. Happy to see this help others as well.

Hi all I need help. I have use 2023-07 api and now it show “The field ‘data’ of the record wasn’t found.”

can you help me fix it or convert it

let
Source = Web.Contents(“https://api.monday.com/” & “v2”,
[
Headers=[
#“Method”=“POST”,
#“Content-Type”=“application/json”,
#“Authorization”=“Bearer mykey”
],
Content=Text.ToBinary(“{”“query”“: “”{ boards (ids: 3796729499) { items { id name column_values { title text } parent_item {id} } } }”“}”)
]
),
#“JSON” = Json.Document(Source,65001),
data = JSON[data],
boards = data[boards],
boards1 = boards{0},
items = boards1[items],
#“Converted to Table” = Table.FromList(items, Splitter.SplitByNothing(), null, null, ExtraValues.Error),

and another table is

Content=Text.ToBinary(“{”“query”“: “”{ boards (ids: 3796729500) { items { id name column_values { title text } } } }”“}”)


I’m trying with all I know but not working. thanks in advance

Hello there @suriyapong,

I do not have experience using Power BI, but on monday’s end (regarding the API), you need to switch from a boards → items structure to a boards → items_page → items structure as shown here!

I hope that helps!

Cheers,
Matias

Hi @JustAnotherMonday , Looking for your advice: The code has been working perfectly for the past year, but today it’s returning errors. My board count hasn’t changed, so it seems the issue might be elsewhere.

Error is “An error occurred in the ‘’ query. DataSource.Error: Web.Contents failed to get contents from ‘https://api.monday.com/v2’ (429): Too Many Requests
Details:
DataSourceKind=Web
DataSourcePath=https://api.monday.com/v2
Url=https://api.monday.com/v2

Hi @ASYADAV5 ,

I would focus on the fact your error is 429 for too many requests. Error handling

Do you know how many pages you are pulling per run of this function?
How many records?
Do you have multiple versions or copies of that 1 query and maybe other queries doing other calls? Also consider if you have multiple environments/workspaces/datasets.

I assume this query limit may apply across all of your Monday API calls within a minute. Could your combined count be close to 5,000 in 1 minute? I’m just making an unverified guess that 5,000 queries means each page of results can be 1 query. That could change if Monday considers each record in the page as a query.

I hope this helps kickstart a successful troubleshooting.

Sorry I never saw your response on this thread. Did you find your solution? Please provide info again if still having issues, so I don’t make suggestions based on old code.

Hi Monday Community,
Thank you so much for this post. I am just starting out with M coding, and my next challenge is to bring in more than 500 items from a board across to Power BI/Query.
I understand this involves concepts related to pagination, cursor, items_page, looping.
Do you think if I edit this code for items=500 it would work for me to retrieve all board items?
I was trying to work with this pbix file but I am facing credentials authentication related issues because I am using this base url: https://api.monday.com/v2.

Anyway, here is my code so far… if anybody can please offer me guidance as to how I can incorporate the aforementioned concepts for me to be able to increase the board items I have:
let
BaseURL = “https://api.monday.com/v2”,
Query = “{”“query”“: “”{ boards(ids: My Board ID) { items_page (limit:500) { cursor items { name column_values { column { title } text text …on MirrorValue { display_value } …on DependencyValue { display_value } …on BoardRelationValue { display_value } } } } } }”“}”,
Headers = [
#“Method”=“POST”,
#“API-Version”=“2024-01”,
#“Content-Type”=“application/json”,
#“Authorization”=“My Token”
],
Source = Web.Contents(BaseURL, [Headers = Headers, Content = Text.ToBinary(Query)]),
JSON = Json.Document(Source, 65001),
data = JSON[data],
boards = data[boards],
boards1 = boards{0},
items_page = boards1[items_page],
items = items_page[items],
#“Converted to Table” = Table.FromList(items, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#“Show ID” = Table.ExpandRecordColumn(#“Converted to Table”, “Column1”, {“name”, “column_values”}, {“Column1.name”, “Column1.column_values”}),
#“Expanded Column1.column_values2” = Table.ExpandListColumn(#“Show ID”, “Column1.column_values”),
#“Expanded Column1.column_values3” = Table.ExpandRecordColumn(#“Expanded Column1.column_values2”, “Column1.column_values”, {“column”, “text”, “display_value”}, {“Column1.column_values.column”, “Column1.column_values.text”, “Column1.column_values.display_value”}),
#“Expanded Column1.column_values.column1” = Table.ExpandRecordColumn(#“Expanded Column1.column_values3”, “Column1.column_values.column”, {“title”}, {“Column1.column_values.column.title”}),
#“Added Value Column” = Table.AddColumn(
#“Expanded Column1.column_values.column1”,
“Value”,
each
let
rawText = [Column1.column_values.text],
displayText = [Column1.column_values.display_value],
safeText = if Value.Is(rawText, type list) then Text.Combine(List.Transform(rawText, each Text.From()), ", ") else Text.From(rawText),
safeDisplay = if Value.Is(displayText, type list) then Text.Combine(List.Transform(displayText, each Text.From(
)), ", ") else Text.From(displayText)
in
if safeText = null or safeText = “” then safeDisplay else safeText
),
#“RenamedColumns1” = Table.RenameColumns(#“Expanded Column1.column_values.column1”,{{“Column1.name”, “Name”}, {“Column1.column_values.column.title”, “Title”}, {“Column1.column_values.text”, “RawValue”}, {“Column1.column_values.display_value”, “DisplayValue”}}),
#“ReversedRows” = Table.ReverseRows(#“RenamedColumns1”),
#“ReversedRows1” = Table.ReverseRows(#“ReversedRows”),
#“Filtered Rows3” = Table.SelectRows(ReversedRows1, each (([Title] <> “Division” and [Title] <> “Family”) or ([Title] = “Division” and [RawValue] <> null) or ([Title] = “Family” and [RawValue] <> null))),
#“AddedValueColumn” = Table.AddColumn(#“Filtered Rows3”, “Value”, each if [RawValue] = null then [DisplayValue] else [RawValue]),
#“ReorderedColumns” = Table.ReorderColumns(#“AddedValueColumn”,{“Title”, “Name”, “RawValue”, “Value”}),
#“RemovedUnusedColumns” = Table.RemoveColumns(#“ReorderedColumns”,{“RawValue”, “DisplayValue”}),
PivotedTable = Table.Pivot(#“RemovedUnusedColumns”, List.Distinct(#“RemovedUnusedColumns”[Title]), “Title”, “Value”)
in
PivotedTable

I would very much appreciate any help that I can get