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:
- The
Method
is now set toPATCH
. - There is a
Body
parameter that contains a JSON object. A standard PowerShell object is created but converted to JSON with theConvertTo-JSON
cmdlet set to the max depth of100
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_text
in 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 theCmdletBinding
declaration allows operations to be wrapped in anIf
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 ofID
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!