Building a Notion PowerShell Module: Part 2

Published:27 October 2023 - 7 min. read

Building on the previous Notion tutorial, you have learned how to create a Notion integration token, retrieve blocks from the Notion API, and wrap all of that into an advanced PowerShell function.

As valuable as getting a page’s blocks is, modifying or updating the blocks on a page is a stepping stone to building practical tools and integrations with other content. In this tutorial, learn how to add, update, and delete blocks through the Notion API, slowly building up a proper Notion API module in PowerShell!

Prerequisites

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

Blocks Everywhere

Like the first tutorial, it’s best to start with some stripped-down code to learn the basics, before wrapping everything into more advanced functions. To add a new block to an existing page, you will use the same API call to retrieve blocks, but with a different HTML method, PATCH.

To tell Notion where you want the block to be, you must pass either the page ID or a block ID, if you want the block to be a child of a different block. As you can see from the below code, it is very similar to that of retrieving blocks. The difference is two-fold:

  1. The Method is now set to PATCH.
  2. There is a Body parameter that contains a JSON object. A standard PowerShell object is created but converted to JSON with the ConvertTo-JSON cmdlet set to the max depth of 100 to avoid issues with creation.
$APIKey     = 'secret_fMnSn52qUruc1k0M7CargoM94mgS3loo3vdVBSzq74W'
$APIURI     = 'https://api.notion.com/v1'
$APIVersion = '2022-06-28'
$GUID       = 'a2b3646d-e941-4df4-874d-56153139b618'

$Params = @{
    "Headers" = @{
        "Authorization"  = "Bearer {0}" -F $APIKey
        "Content-type"   = "application/json"
        "Notion-Version" = "{0}" -F $APIVersion
    }
    "Method"  = 'PATCH'
    "URI"     = ("{0}/blocks/{1}/children" -F $APIURI, [GUID]::new($GUID))
    "Body"    = @{
        "children" = @(
            @{
                "paragraph" = @{
                    "rich_text" = @(
                        @{
                            "text" = @{
                                "content" = "This is written by a robot!"
                            }
                        }
                    )
                }
            }
        )
    } | ConvertTo-JSON -Depth 100
}

$Result = Invoke-RestMethod @Params

There is no output from a successful call with this code, but you can immediately see the results of the call. On the left is the content before the code is run, and on the right is after.

What if you wanted to create a block as the child of another block? You can leverage the previously created cmdlet, Get-NotionBlock, to find the last block ID and pass that into the code to create the new block. The code changes you are going to make are two lines. The first code addition retrieves all page blocks using the previously created function.

Next, instead of passing the page ID, you will pass the results of the $Blocks object, but using array notation to find the last one with the [-1] notation, and get the id. When you rerun the code, with these changes, the final block will have a new child block, as shown.

$Blocks = Get-NotionBlock -GUID 'a2b3646de9414df4874d56153139b618'
$GUID   = $Blocks[-1].id

Creating a Fancier Block

So far, you have created a paragraph block with just plain text. How about creating a callout with rich text objects contained within? The same structure as before will be used, with the page ID, but creating a more complex JSON object.

Using the same code as above, you are replacing the Body parameter with the below code, which will create a callout, set the background color, and create bold initial content.

@{
    "children" = @(
        @{
            "callout" = @{
                "rich_text" = @(
                    @{
                        "text" = @{
                            "content" = "Just a friendly reminder of the three laws of robotics."
                        }
                        "annotations" = @{
                            "bold" = $True
                        }
                    }
                )
                "color" = "blue_background"
            }
        }
    )
}

You may have noticed that the text there references the three laws of robotics, but the callout block can only support rich_textin the initial creation. Thankfully, you have already learned how to append child blocks to an existing one!

To fix this, you will create the list block as a child of the callout block. Before that, you must retrieve the callout and the ID associated with the block. To do this, you will use the Get-NotionBlock function and filter the results to the callout type, finally selecting the id.

$Blocks = Get-NotionBlock -GUID 'a2b3646de9414df4874d56153139b618'
$Blocks | Where-Object type -EQ 'callout' | Select-Object id

With the ID in hand, craft the new block, changing the $GUID value to the ID you previously located. Run the code, and you will see the following result.

"Body" = @{
    "children" = @(
        @{
            "bulleted_list_item" = @{
                "rich_text" = @(
                    @{
                        "text" = @{
                            "content" = "A robot may not injure a human being or, through inaction, allow a human being to come to harm."
                        }
                    }
                )                
            }
        }
        @{
            "bulleted_list_item" = @{
                "rich_text" = @(  
                    @{
                        "text" = @{
                            "content" = "A robot must obey the orders given it by human beings except where such orders would conflict with the First Law."
                        }
                    }
                )
            }
        }
        @{
            "bulleted_list_item" = @{
                "rich_text" = @(  
                    @{
                        "text" = @{
                            "content" = "A robot must protect its own existence as long as such protection does not conflict with the First or Second Law."
                        }
                    }
                )
            }
        }
    )
} | ConvertTo-JSON -Depth 100

Updating an Existing Block

With all the blocks created, a more in-your-face warning for the three laws would be warranted. Instead of removing and re-creating, updating the content of the callout is doable.

The change is to the API URL and the body code. Change the API to: ("{0}/blocks/{1}" -F $APIURI, [GUID]::new($GUID)), which removes the children from the end. This API call also uses the PATCH method as well.

"Body"    = @{
    "callout" = @{
        "rich_text" = @(
            @{
                "text" = @{
                    "content" = "A strong reminder of the three laws of robotics."
                }
                "annotations" = @{
                    "bold" = $True
                    "color" = 'red_background'
                }
            }
        )
    }
} | ConvertTo-JSON -Depth 100

Removing a Block

With all these changes, you may need to remove one. To do so is similar to the previous API calls. This time, you will use the DELETE method, which sets a block (or page) to be archived and in the trash (making the content recoverable).

Run the following to remove the previously located callout block. Which will remove it from the page.

$APIKey     = 'secret_fMnSn52qUruc1k0M7CargoM94mgS3loo3vdVBSzq74W'
$APIURI     = 'https://api.notion.com/v1'
$APIVersion = '2022-06-28'
$GUID       = '97b47389-befa-43d5-a2ee-b08f3ae602f9'

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

$Result = Invoke-RestMethod @Params

Bringing it All Together

Like the previous tutorial, to continue building out this PowerShell Notion module, it’s time to wrap the code into advanced functions and add to the module. The three different functions that you are creating are:

  • Creating a new block – New-NotionBlock
  • Updating an existing block – Set-NotionBlock
  • Removing an existing block – Remove-NotionBlock

Creating new Blocks through the New-NotionBlock Function

Similar to the original advanced function you have created, here is the New-NotionBlock function in all its glory. The significant changes are the addition of support for:

  • What If – Adding SupportsShouldProcess = $True to the CmdletBinding declaration allows operations to be wrapped in an If statement for $PSCmdlet.ShouldProcess to see what operation will occur first.
  • Pipeline Input – For the $GUID parameter, support pipeline input by the [Parameter(ValueFromPipelineByPropertyName = $True)] declaration.
  • Pipeline Aliases – To ensure that the incoming ID of an object passes to the right place, give the $GUID parameter an alias of ID that works in conjunction with the pipeline values.

With all of that in place, the function now creates a new block based on the GUID of the page, or parent block, and the declared content. This outputs the created block for later use in the pipeline.

Function New-NotionBlock {
  [CmdletBinding(SupportsShouldProcess = $True)]

  Param(
    [String]$APIKey,
    [String]$APIVersion,
    [ValidateScript( { [System.URI]::IsWellFormedUriString( $_ ,[System.UriKind]::Absolute ) } )][String]$APIURI,

    [Parameter(ValueFromPipelineByPropertyName = $True)]
    [Alias("ID")]
    [ValidateScript( { Try { If ( [GUID]::Parse( $_ ) ) { $True } } Catch { $False } } )][String]$GUID,

    $Content
  )

  Process {
    $Params = @{
      "Headers" = @{
        "Authorization"  = "Bearer {0}" -F $APIKey
        "Content-type"   = "application/json"
        "Notion-Version" = "{0}" -F $APIVersion
      }
      "Method" = 'PATCH'
      "URI"    = ("{0}/blocks/{1}/children" -F $APIURI, [GUID]::new($GUID))
      "Body"   = $Content | ConvertTo-JSON -Depth 100
    }

    Write-Verbose "[Process] Params: $($Params | Out-String)"

    If ($PSCmdlet.ShouldProcess($GUID,"Adding Block")) {
      Try {
        $Result = Invoke-RestMethod @Params -ErrorAction 'Stop'
      } Catch {
        $Message = ($Error[0].ErrorDetails.Message | ConvertFrom-JSON).message

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

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

Update Blocks with the Set-NotionBlock Function

Similar to creating a block, the Set-NotionBlock function replaces the existing content of a block with that of the new content you define. The structure of the function is nearly identical with the only change being the API call itself.

Function Set-NotionBlock {
  [CmdletBinding(SupportsShouldProcess = $True)]

  Param(
    [String]$APIKey,
    [String]$APIVersion,
    [ValidateScript( { [System.URI]::IsWellFormedUriString( $_ ,[System.UriKind]::Absolute ) } )][String]$APIURI,

    [Parameter(ValueFromPipelineByPropertyName = $True)]
    [Alias("ID")]
    [ValidateScript( { Try { If ( [GUID]::Parse( $_ ) ) { $True } } Catch { $False } } )][String]$GUID,

    $Content
  )

  Process {
    $Params = @{
      "Headers" = @{
        "Authorization"  = "Bearer {0}" -F $APIKey
        "Content-type"   = "application/json"
        "Notion-Version" = "{0}" -F $APIVersion
      }
      "Method" = 'PATCH'
      "URI"    = ("{0}/blocks/{1}" -F $APIURI, [GUID]::new($GUID))
      "Body"   = $Content | ConvertTo-JSON -Depth 100
    }

    Write-Verbose "[Process] Params: $($Params | Out-String)"

    If ($PSCmdlet.ShouldProcess($GUID,"Updating Block")) {
      Try {
        $Result = Invoke-RestMethod @Params -ErrorAction 'Stop'
      } Catch {
        $Message = ($Error[0].ErrorDetails.Message | ConvertFrom-JSON).message

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

    $Result
  }
}

Removing Old Blocks with the Remove-NotionBlock Function

Finally, the Remove-NotionBlock function rounds out the functions with the ability to remove a block but again with a similar structure to the prior functions. The primary difference is that the result of the operation is not output, as there is none.

Function Remove-NotionBlock {
  [CmdletBinding(SupportsShouldProcess = $True)]

  Param(
    [String]$APIKey,
    [String]$APIVersion,
    [ValidateScript( { [System.URI]::IsWellFormedUriString( $_ ,[System.UriKind]::Absolute ) } )][String]$APIURI,

    [Parameter(ValueFromPipelineByPropertyName = $True)]
    [Alias("ID")]
    [ValidateScript( { Try { If ( [GUID]::Parse( $_ ) ) { $True } } Catch { $False } } )][String]$GUID
  )

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

    Write-Verbose "[Process] Params: $($Params | Out-String)"

    If ($PSCmdlet.ShouldProcess($GUID,"Removing Block")) {
      Try {
        $Result = Invoke-RestMethod @Params -ErrorAction 'Stop'
      } Catch {
        $Message = ($Error[0].ErrorDetails.Message | ConvertFrom-JSON).message

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

Seeing it All in Action

What does this look like when you use everything together? You can create a block, update a block, and ultimately remove the block, by piping the content to each function. Here, it helps to sleep for a few seconds after the update operation to see the removal in action.

$Block = New-NotionBlock -GUID 'a2b3646de9414df4874d56153139b618' -Content @{
    "children" = @(
        @{
            "paragraph" = @{
                "rich_text" = @(
                    @{
                        "text" = @{
                            "content" = "This is written by a robot!"
                        }
                    }
                )
            }
        }
    )
} | Set-NotionBlock -Content @{
    "paragraph" = @{
        "rich_text" = @(
            @{
                "text" = @{
                    "content" = "This is UPDATED by a robot!"
                }
            }
        )
    }
}

Start-Sleep -Seconds 3

$Block | Remove-NotionBlock

Next Time in Building a PowerShell Notion Module

With the addition of these three functions and the previously created function, you now have a full set of functions to manipulate blocks as much as you want. In the next article of the series, you will learn how to work with Databases and Pages!

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!