Building a Notion PowerShell Module: Part 1

Published:18 October 2023 - 6 min. read

This tutorial will teach you how to create an advanced function to interact with the Notion API. Notion is a powerful web application to manage knowledge in flexible workspaces. Take your automation to the next level with Notion PowerShell integration!

Prerequisites

To follow along in this tutorial, you only need a Notion account and PowerShell; here, PowerShell v7.3.7 is in use.

Creating a Notion Integration Token

Once signed into Notion, open a browser to the “My integrations” page. Here, you will create a new integration. The resulting secret key will be used in every request, authenticating your REST API calls.

1. Once on the My Integrations page, click the “Create new integration” placeholder or “+ New integration” button.

Creating a new integration.
Creating a new integration.

2. Next, fill the details out accordingly. Choose the Associated Workspace and give a Name to identify the integration. Optionally, upload a Logo to differentiate your integration further.

Entering the necessary integration details.
Entering the necessary integration details.

3. Click the eye icon to show the integration secret. Copy this integration secret for later use in the PowserShell script.

Retrieving the integration secret.
Retrieving the integration secret.

4. Navigate to the Capabilities section and make any necessary changes, here, the defaults are used.

Modifying the integration capabilities.
Modifying the integration capabilities.

Interacting with the Notion API in PowerShell

With the integration created and the secret key in hand, navigate to the given page you will interact with. You may give an integration access to the top-level, or root, of a series of pages. When granted access to the root of a series of pages, all child pages will also be accessible by the integration.

In this example, a PowerShell Testing page has been created. You can add the integrations access through the Connections menu by clicking on the three dots in the upper-right corner to access additional menu options. Highlight the “Add connections” and choose your newly created integration named “PowerShell.”

Granting the integration access to a given page.
Granting the integration access to a given page.

A confirmation will show, and as noted, will ask if you grant the integration access to all child pages as well.

Confirming the integration access.
Confirming the integration access.

Retrieving the Pages Blocks

Everything in Notion is based on the idea of blocks. A paragraph is considered a block, even an empty one. Since this page has no content, you can request the content, and the default empty paragraph block will be returned.

You will need several pieces of information to create a REST API call to Notion.

  • API Key – The previously created integration and saved API key.
  • API URI – The URL of the REST API.
  • API Version – Breaking changes can, and do, occur from time to time in the REST API. You need to give the API version you are operating against. The current version is 2022-06-28 at the time of this article’s creation.
  • Page Size – You can only request so many blocks in a single REST API call, and the maximum for the child blocks call is 100.
  • GUID (Globally Unique Identifier) – Each Notion page is identified by its GUID, the value appended after friendly text. You can copy and paste that value from the URL. For example, the page is PowerShell-Testing-a2b3646de9414df4874d56153139b618, but the GUID is a2b3646de9414df4874d56153139b618.

💡 Although the secret key is shown, it has since been removed and is no longer valid.

With all those pieces in hand, it’s time to create your REST API call. Like most REST API calls in PowerShell, you will call the Invoke-RestMethod cmdlet. The parameters are laid out using parameter splatting for readability, and the request results are stored in the $Result variable. To see the results, the results property is requested at the end.

$APIKey     = 'secret_fS2A21NwTL4L6XWFTTdgmF3xboWiYXExKZ8Pw15oRMw'
$APIURI     = 'https://api.notion.com/v1'
$APIVersion = '2022-06-28'
$PageSize   = '100'
$GUID       = 'a2b3646de9414df4874d56153139b618'

$Params = @{
    "Headers" = @{
        "Authorization"  = "Bearer {0}" -F $APIKey
        "Content-type"   = "application/json"
        "Notion-Version" = "{0}" -F $APIVersion
    }
    "Method"  = 'GET'
    "URI"     = ("{0}/blocks/{1}/children?page_size={2}" -F $APIURI, [GUID]::new($GUID), $PageSize)
}

$Result = Invoke-RestMethod @Params

$Result.results
Showing the results of a REST API call to Notion.
Showing the results of a REST API call to Notion.

As you can see, a single block was returned with detailed information. The actual content, though blank, is within the paragraph property and the rich_text property. Right now, there is nothing contained within.

If the text “Hello from Notion!” is entered in the test page, and the code is rerun, then the returned rich_text value will reflect that.

Displaying the rich text of a Notion block.
Displaying the rich text of a Notion block.

Creating an Advanced PowerShell Function

With the basics in hand, what are the next steps? To make a more robust reusable function, you can leverage the tenets of advanced PowerShell function. The improvements are less code to run on the command line, easier script use, and more validation. In addition, with the page_size limit of 100, a page over that will need a recursive call to retrieve all of the results, which this advanced function can do.

With the intent in mind, how does the code work? For any advanced function, you must include the [CmdletBinding()] underneath the function declaration. This indicates to PowerShell that you may use the advanced features.

Next, the Param code block is declared. For two parameters, $APIURI, and $GUID, advanced validation is done via the ValidateScript decorator. Using the [System.URI]::IsWellFormedUriString method, this validates that the passed value is an accurate URI. The [GUID]::Parse method on the [GUID] type accelerator tests if the passed-in value is a true GUID.

In the Begin block, instead of redefining the parameters passed to Invoke-RestMethod on every iteration, they are defined once at the start of the pipeline. The tricker parts are in the Process block. The start_cursor parameter for Notion defines where the start of the result gathering begins. If defined, the previous values will be skipped, and the following 100 blocks will be counted from there.

In this function, that is only defined on a recursive call. For example, Notion will return a true or false $Results.has_more value if more than 100 blocks exist. If this is true, call the same Get-NotionBlock function, pass in the same parameters via the $PSBoundParameters special variable, and give the $Result.next_cursor value to the -StartCursor parameter. This will continue to do so until has_more is false.

Similarly, this is how the has_children property works, as child blocks may need to be retrieved as well. The function will call itself to retrieve those before continuing.

Function Get-NotionBlock {
    [CmdletBinding()]

    Param(
        [String]$APIKey,
        [String]$APIVersion,
        [ValidateScript( { [System.URI]::IsWellFormedUriString( $_ ,[System.UriKind]::Absolute ) } )][String]$APIURI,
        [ValidateScript( { Try { If ( [GUID]::Parse( $_ ) ) { $True } } Catch { $False } } )][String]$GUID,
        [String]$StartCursor,
        [Int]$PageSize = 100
    )

    Begin {
        $Params = @{
            "Headers" = @{
                "Authorization"  = "Bearer {0}" -F $APIKey
                "Content-type"   = "application/json"
                "Notion-Version" = "{0}" -F $APIVersion
            }
            "Method"  = 'GET'
        }
    }

    Process {
        Try {
            If ($StartCursor) {
                $Params.Add("URI" , ("{0}/blocks/{1}/children?start_cursor={3}&page_size={2}" -F $APIURI, [GUID]::new($GUID), $PageSize, $StartCursor))
            } Else {
                $Params.Add("URI" , ("{0}/blocks/{1}/children?page_size={2}" -F $APIURI, [GUID]::new($GUID), $PageSize))
            }

            Write-Verbose "[Process] Params: $($Params | Out-String)"
            
            $Result = Invoke-RestMethod @Params

            If ($Result.has_more) {
                $Result.results

                If ([Bool]($Result.results | Where-Object has_children -EQ $True)) {
                    $Result.results | Where-Object has_children -EQ $True | ForEach-Object { Get-NotionBlock @PSBoundParameters -StartCursor:$Null -GUID $PSItem.id }
                }

                Write-Verbose "[Process] More Results Exist"

                Get-NotionBlock @PSBoundParameters -StartCursor $Result.next_cursor
            } Else {
                $Result.results

                If ([Bool]($Result.results | Where-Object has_children -EQ $True)) {
                    $Result.results | Where-Object has_children -EQ $True | ForEach-Object { Get-NotionBlock @PSBoundParameters -StartCursor:$Null -GUID $PSItem.id }
                }
            }
        } Catch {
            $Message = ($Error[0].ErrorDetails.Message | ConvertFrom-JSON).message

            Write-Error "Command Failed to Run: $Message"
        }
    }
}

With the advanced function defined, it’s time to call the function and retrieve the blocks! Create the $Params variable and pass that splatted variable into the Get-NotionBlock function. Here, only the first object is shown.

$Params = @{
    "APIKey"     = 'secret_fS2A21NwTL4L6XWFTTdgmF3xboWiYXExKZ8Pw15oRMw'
    "APIVersion" = '2022-06-28'
    "APIURI"     = 'https://api.notion.com/v1'
    "GUID"       = 'a2b3646de9414df4874d56153139b618'
}

Get-NotionBlock @Params | Select-Object -First 1
Showing the first block of the retrieved results.
Showing the first block of the retrieved results.

What if the page has over 100 blocks? When that happens, the function runs recursively. By passing in the -Verbose parameter, you can see how the start_cursor changes. To keep the results concise, pass to the Measure-Object cmdlet to show the over 158 results returned. You can tell by the verbose output that the start_cursor is defined in the second call and that more results exist.

Displaying the results of a page with over 100 blocks.
Displaying the results of a page with over 100 blocks.

Bonus! Reduce Passed Parameters Through PSDefaultParameterValues

You may have noticed that in the $Params block, you are passing the API Key, API Version, and API URI. As these three values very rarely change, it would be convenient not to have to do that every time. PowerShell has a mechanism to make this easier.

With the $PSDefaultParameterValues variable, you can define default parameter values once placed in your profile. Every time a function is called, these values will be given if no other value is defined at runtime.

$PSDefaultParameterValues = @{
    "Get-NotionBlock:APIKey"     = 'secret_fS2A21NwTL4L6XWFTTdgmF3xboWiYXExKZ8Pw15oRMw'
    "Get-NotionBlock:APIVersion" = '2022-06-28'
    "Get-NotionBlock:APIURI"     = 'https://api.notion.com/v1'
}

Making the same call as before, but simply passing the GUID in means that you can use the function even easier!

Utilizing the $PSDefaultParameterValues variable.
Utilizing the $PSDefaultParameterValues variable.

Wrapping Up

This is just the start of your Notion PowerShell journey! This article showcases a single REST API call but lays the groundwork for more possibilities to come. Integrate Notion into your PowerShell scripts and take advantage of both technologies’ flexibility and power.

The same principles in play for this article hold for implementing all of the other methods, which will be shown in future articles!

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!