diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,30 @@
 # Changelog for notion-client
 
+## 0.7.0.0 (2026-04-16)
+
+### Breaking Changes
+* `BlockContent` gains new constructors: `Heading4`, `Tab`, `MeetingNotes`, `Template` — pattern matches on `BlockContent` must handle these variants
+* `MentionContent` gains new constructors: `TemplateMentionDate`, `TemplateMentionUser` — pattern matches must be updated
+* `RollupFunction` gains new constructors: `CountPerGroup`, `PercentPerGroup`, `Unique`
+* `DateCondition` gains new constructors: `DateThisMonth`, `DateThisYear`
+* `PageObject` gains new fields: `isLocked`, `isArchived`
+* `DataSourceParent` gains `parentDatabaseId` field
+* `ColumnBlock` gains `widthRatio` field
+* `BotUser` gains `workspaceId`, `workspaceLimits` fields (new `WorkspaceLimits` type)
+* `CreateComment` gains `attachments` and `displayName` fields
+* `UpdatePage` gains `isLocked` and `isArchived` fields
+* `CommentAttachment` restructured: all fields now `Maybe`, new `category` field, hand-rolled FromJSON/ToJSON to support both read and write shapes
+* `CommentDisplayName` gains `resolvedName` field
+
+### New Features
+* Add `retrievePageFiltered` method with `filter_properties` query parameter for `GET /v1/pages/{page_id}` (`retrievePage` remains backward-compatible)
+* Smart constructor `headingBlock` now supports level 4
+* `CommentAttachment` and `CommentDisplayName` now have `ToJSON` instances
+
+### Bug Fixes
+* Strip read-only `list_start_index` and `list_format` fields from `BlockUpdate` serialization — the Notion API rejects PATCH requests containing these fields on `numbered_list_item` blocks
+* Fix parsing of `List Comments` responses: `CommentAttachment` now correctly decodes the `{"category":..., "file":...}` read shape, and `CommentDisplayName` preserves the `resolved_name` field
+
 ## 0.6.1.0 (2026-03-31)
 
 ### New Features
diff --git a/notion-client-example/DatabaseDemo.hs b/notion-client-example/DatabaseDemo.hs
--- a/notion-client-example/DatabaseDemo.hs
+++ b/notion-client-example/DatabaseDemo.hs
@@ -175,7 +175,7 @@
       -- In API version 2025-09-03, pages are created under a data source
       createPageRequest =
         CreatePage
-          { parent = DataSourceParent {dataSourceId = dsId}, -- Specify parent data source
+          { parent = DataSourceParent {dataSourceId = dsId, parentDatabaseId = Nothing}, -- Specify parent data source
             properties = pageProperties, -- Required page properties
             children = Just initialBlocks, -- Optional initial content
             markdown = Nothing, -- Could use markdown instead of children
diff --git a/notion-client-example/TemplateDemo.hs b/notion-client-example/TemplateDemo.hs
--- a/notion-client-example/TemplateDemo.hs
+++ b/notion-client-example/TemplateDemo.hs
@@ -81,7 +81,7 @@
           -- template variables resolve.
           createReq =
             CreatePage
-              { parent = DataSourceParent {dataSourceId = dsId},
+              { parent = DataSourceParent {dataSourceId = dsId, parentDatabaseId = Nothing},
                 properties = props,
                 children = Nothing,
                 markdown = Nothing,
diff --git a/notion-client.cabal b/notion-client.cabal
--- a/notion-client.cabal
+++ b/notion-client.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.4
 name:               notion-client
-version:            0.6.1.0
+version:            0.7.0.0
 synopsis:           Type-safe Haskell client for the Notion API
 description:
   This package provides comprehensive and type-safe bindings
diff --git a/src/Notion/V1.hs b/src/Notion/V1.hs
--- a/src/Notion/V1.hs
+++ b/src/Notion/V1.hs
@@ -16,9 +16,9 @@
 --
 --     clientEnv <- getClientEnv "https://api.notion.com/v1"
 --
---     let Methods{ retrievePage } = makeMethods clientEnv (Text.pack token)
+--     let methods = makeMethods clientEnv (Text.pack token)
 --
---     page <- retrievePage "page-id-here"
+--     page <- retrievePage methods "page-id-here"
 --
 --     print page
 -- @
@@ -97,7 +97,7 @@
                  :<|> queryDataSource
                  :<|> listDataSourceTemplates_
                )
-        :<|> ( retrievePage
+        :<|> ( retrievePageFiltered
                  :<|> createPage
                  :<|> updatePage
                  :<|> retrievePageProperty
@@ -146,6 +146,9 @@
           Nothing -> Exception.throwIO err
         Right a -> return a
 
+    -- Wrap retrievePageFiltered to provide backward-compatible retrievePage
+    retrievePage pid = retrievePageFiltered pid []
+
     -- Keep the ListOf structure
     listBlockChildren = retrieveBlockChildren_
     listUsers = listUsers_
@@ -185,6 +188,8 @@
     -- \* Pages
     createPage :: CreatePage -> IO PageObject,
     retrievePage :: PageID -> IO PageObject,
+    -- | Retrieve a page, optionally filtering which properties are returned.
+    retrievePageFiltered :: PageID -> [Text] -> IO PageObject,
     updatePage :: PageID -> UpdatePage -> IO PageObject,
     -- | Retrieve a single page property item.
     -- For title, rich_text, relation, and people properties, the response may be paginated.
diff --git a/src/Notion/V1/BlockContent.hs b/src/Notion/V1/BlockContent.hs
--- a/src/Notion/V1/BlockContent.hs
+++ b/src/Notion/V1/BlockContent.hs
@@ -533,7 +533,8 @@
       }
   | -- | Single column within a column list.
     ColumnBlock
-      { children :: Vector BlockContent
+      { widthRatio :: Maybe Double,
+        children :: Vector BlockContent
       }
   | -- | Table block.
     --
@@ -563,6 +564,32 @@
       { syncedFrom :: SyncedFrom,
         children :: Vector BlockContent
       }
+  | -- | Heading level 4.
+    --
+    -- Children are only accepted by the API when @isToggleable@ is @True@.
+    Heading4Block
+      { richText :: Vector RichText,
+        color :: Color,
+        isToggleable :: Bool,
+        children :: Vector BlockContent
+      }
+  | -- | Tab block (container).
+    TabBlock
+      { children :: Vector BlockContent
+      }
+  | -- | Meeting notes block (read-only).
+    MeetingNotesBlock
+      { meetingTitle :: Text,
+        meetingStatus :: Maybe Text,
+        calendarEvent :: Maybe Value,
+        recording :: Maybe Value,
+        children :: Vector BlockContent
+      }
+  | -- | Template block (deprecated, but still returned by the API).
+    TemplateBlock
+      { richText :: Vector RichText,
+        children :: Vector BlockContent
+      }
   | -- | Unsupported block type returned by the API.
     UnsupportedBlock
   | -- | Fallback for block types not yet modeled.
@@ -689,7 +716,11 @@
   ColumnListBlock {..} ->
     ("column_list", object $ childrenPairs children)
   ColumnBlock {..} ->
-    ("column", object $ childrenPairs children)
+    ( "column",
+      object $
+        maybe [] (\r -> ["width_ratio" .= r]) widthRatio
+          <> childrenPairs children
+    )
   TableBlock {..} ->
     ( "table",
       object $
@@ -708,6 +739,29 @@
         ["synced_from" .= syncedFrom]
           <> childrenPairs children
     )
+  Heading4Block {..} ->
+    ( "heading_4",
+      object $
+        ["rich_text" .= richText, "color" .= color, "is_toggleable" .= isToggleable]
+          <> childrenPairs children
+    )
+  TabBlock {..} ->
+    ("tab", object $ childrenPairs children)
+  MeetingNotesBlock {..} ->
+    ( "meeting_notes",
+      object $
+        ["title" .= meetingTitle]
+          <> maybe [] (\s -> ["status" .= s]) meetingStatus
+          <> maybe [] (\ce -> ["calendar_event" .= ce]) calendarEvent
+          <> maybe [] (\r -> ["recording" .= r]) recording
+          <> childrenPairs children
+    )
+  TemplateBlock {..} ->
+    ( "template",
+      object $
+        ["rich_text" .= richText]
+          <> childrenPairs children
+    )
   UnsupportedBlock ->
     ("unsupported", object [])
   UnknownBlock typeName val ->
@@ -830,6 +884,7 @@
     children <- fromMaybe Vector.empty <$> o .:? "children"
     pure ColumnListBlock {..}
   "column" -> parseObj $ \o -> do
+    widthRatio <- o .:? "width_ratio"
     children <- fromMaybe Vector.empty <$> o .:? "children"
     pure ColumnBlock {..}
   "table" -> parseObj $ \o -> do
@@ -851,6 +906,26 @@
     syncedFrom <- o .: "synced_from"
     children <- fromMaybe Vector.empty <$> o .:? "children"
     pure SyncedBlockContent {..}
+  "heading_4" -> parseObj $ \o -> do
+    richText <- o .: "rich_text"
+    color <- fromMaybe Default <$> o .:? "color"
+    isToggleable <- fromMaybe False <$> o .:? "is_toggleable"
+    children <- fromMaybe Vector.empty <$> o .:? "children"
+    pure Heading4Block {..}
+  "tab" -> parseObj $ \o -> do
+    children <- fromMaybe Vector.empty <$> o .:? "children"
+    pure TabBlock {..}
+  "meeting_notes" -> parseObj $ \o -> do
+    meetingTitle <- o .: "title"
+    meetingStatus <- o .:? "status"
+    calendarEvent <- o .:? "calendar_event"
+    recording <- o .:? "recording"
+    children <- fromMaybe Vector.empty <$> o .:? "children"
+    pure MeetingNotesBlock {..}
+  "template" -> parseObj $ \o -> do
+    richText <- o .: "rich_text"
+    children <- fromMaybe Vector.empty <$> o .:? "children"
+    pure TemplateBlock {..}
   "unsupported" -> pure UnsupportedBlock
   _ -> pure (UnknownBlock typeName val)
   where
@@ -889,9 +964,19 @@
 
 instance ToJSON BlockUpdate where
   toJSON (BlockUpdate bc) =
-    let (typeName, inner) = blockContentFields bc
+    let bc' = stripReadOnlyFields bc
+        (typeName, inner) = blockContentFields bc'
      in object [Key.fromText typeName .= inner]
 
+-- | Clear fields that the Notion API rejects on PATCH @\/blocks\/:id@.
+--
+-- Both @list_start_index@ and @list_format@ are read-only — the API returns
+-- them in GET responses but rejects them on PATCH (and POST).
+stripReadOnlyFields :: BlockContent -> BlockContent
+stripReadOnlyFields bc = case bc of
+  NumberedListItemBlock {} -> bc {listStartIndex = Nothing, listFormat = Nothing}
+  _ -> bc
+
 -- ---------------------------------------------------------------------------
 -- Smart constructors
 -- ---------------------------------------------------------------------------
@@ -917,10 +1002,11 @@
 paragraphBlock :: Vector RichText -> BlockContent
 paragraphBlock rt = ParagraphBlock rt Default Nothing Vector.empty
 
--- | Create a heading block at the given level (1, 2, or 3; defaults to 3).
+-- | Create a heading block at the given level (1, 2, 3, or 4; defaults to 3).
 headingBlock :: Int -> Vector RichText -> BlockContent
 headingBlock 1 rt = Heading1Block rt Default False Vector.empty
 headingBlock 2 rt = Heading2Block rt Default False Vector.empty
+headingBlock 4 rt = Heading4Block rt Default False Vector.empty
 headingBlock _ rt = Heading3Block rt Default False Vector.empty
 
 -- | Create a bulleted list item block.
@@ -985,4 +1071,8 @@
   ColumnBlock {} -> block {children = cs}
   TableBlock {} -> block {children = cs}
   SyncedBlockContent {} -> block {children = cs}
+  Heading4Block {} -> block {children = cs}
+  TabBlock {} -> block {children = cs}
+  MeetingNotesBlock {} -> block {children = cs}
+  TemplateBlock {} -> block {children = cs}
   _ -> block
diff --git a/src/Notion/V1/Comments.hs b/src/Notion/V1/Comments.hs
--- a/src/Notion/V1/Comments.hs
+++ b/src/Notion/V1/Comments.hs
@@ -12,7 +12,9 @@
   )
 where
 
-import Data.Aeson ((.:), (.:?))
+import Data.Aeson ((.:), (.:?), (.=))
+import Data.Aeson qualified as Aeson
+import Data.Maybe (catMaybes)
 import Notion.Prelude
 import Notion.V1.Common (BlockID, ExternalFile, File, ObjectType (..), Parent, UUID)
 import Notion.V1.ListOf (ListOf)
@@ -24,28 +26,70 @@
 type CommentID = UUID
 
 -- | Comment attachment (files attached to comments)
+--
+-- Unifies the two shapes returned/accepted by the Notion API:
+--
+-- * Read responses (@GET \/v1\/comments@) contain @category@ + @file@.
+-- * Write requests (@POST \/v1\/comments@) contain @name@ + @type@ + @external@\/@file@.
 data CommentAttachment = CommentAttachment
-  { name :: Text,
-    type_ :: Text,
+  { name :: Maybe Text,
+    type_ :: Maybe Text,
+    category :: Maybe Text,
     external :: Maybe ExternalFile,
     file :: Maybe File
   }
   deriving stock (Generic, Show)
 
 instance FromJSON CommentAttachment where
-  parseJSON = genericParseJSON aesonOptions {fieldLabelModifier = \s -> if s == "type_" then "type" else labelModifier s}
+  parseJSON = Aeson.withObject "CommentAttachment" $ \o ->
+    CommentAttachment
+      <$> o .:? "name"
+      <*> o .:? "type"
+      <*> o .:? "category"
+      <*> o .:? "external"
+      <*> o .:? "file"
 
+instance ToJSON CommentAttachment where
+  toJSON CommentAttachment {..} =
+    Aeson.object $
+      catMaybes
+        [ ("name" .=) <$> name,
+          ("type" .=) <$> type_,
+          ("category" .=) <$> category,
+          ("external" .=) <$> external,
+          ("file" .=) <$> file
+        ]
+
 -- | Comment display name (custom display name for comments)
+--
+-- Read responses may include @resolved_name@ (the rendered label for a user
+-- mention); write requests only use @display_name@.
 data CommentDisplayName = CommentDisplayName
   { type_ :: Text,
     emoji :: Maybe Text,
-    displayName :: Maybe Text
+    displayName :: Maybe Text,
+    resolvedName :: Maybe Text
   }
   deriving stock (Generic, Show)
 
 instance FromJSON CommentDisplayName where
-  parseJSON = genericParseJSON aesonOptions {fieldLabelModifier = \s -> if s == "type_" then "type" else labelModifier s}
+  parseJSON = Aeson.withObject "CommentDisplayName" $ \o ->
+    CommentDisplayName
+      <$> o .: "type"
+      <*> o .:? "emoji"
+      <*> o .:? "display_name"
+      <*> o .:? "resolved_name"
 
+instance ToJSON CommentDisplayName where
+  toJSON CommentDisplayName {..} =
+    Aeson.object $
+      ("type" .= type_)
+        : catMaybes
+          [ ("emoji" .=) <$> emoji,
+            ("display_name" .=) <$> displayName,
+            ("resolved_name" .=) <$> resolvedName
+          ]
+
 -- | Notion comment object
 data CommentObject = CommentObject
   { id :: CommentID,
@@ -83,7 +127,9 @@
 data CreateComment = CreateComment
   { parent :: Parent,
     richText :: Vector RichText,
-    discussionId :: Maybe UUID
+    discussionId :: Maybe UUID,
+    attachments :: Maybe (Vector CommentAttachment),
+    displayName :: Maybe CommentDisplayName
   }
   deriving stock (Generic, Show)
 
diff --git a/src/Notion/V1/Common.hs b/src/Notion/V1/Common.hs
--- a/src/Notion/V1/Common.hs
+++ b/src/Notion/V1/Common.hs
@@ -46,7 +46,7 @@
 -- | Parent object that can be a database, data source, page, block, or workspace
 data Parent
   = DatabaseParent {databaseId :: UUID}
-  | DataSourceParent {dataSourceId :: UUID}
+  | DataSourceParent {dataSourceId :: UUID, parentDatabaseId :: Maybe UUID}
   | PageParent {pageId :: UUID}
   | BlockParent {blockId :: UUID}
   | WorkspaceParent {workspace :: Bool}
@@ -65,8 +65,8 @@
       parseByType = \case
         "database" -> fmap DatabaseParent . (.: "database_id")
         "database_id" -> fmap DatabaseParent . (.: "database_id")
-        "data_source" -> fmap DataSourceParent . (.: "data_source_id")
-        "data_source_id" -> fmap DataSourceParent . (.: "data_source_id")
+        "data_source" -> \o -> DataSourceParent <$> o .: "data_source_id" <*> o .:? "database_id"
+        "data_source_id" -> \o -> DataSourceParent <$> o .: "data_source_id" <*> o .:? "database_id"
         "page" -> fmap PageParent . (.: "page_id")
         "page_id" -> fmap PageParent . (.: "page_id")
         "block" -> fmap BlockParent . (.: "block_id")
@@ -77,7 +77,7 @@
       parseByKey :: Object -> Parser Parent
       parseByKey o =
         asum
-          [ DataSourceParent <$> o .: "data_source_id",
+          [ DataSourceParent <$> o .: "data_source_id" <*> o .:? "database_id",
             DatabaseParent <$> o .: "database_id",
             PageParent <$> o .: "page_id",
             BlockParent <$> o .: "block_id",
@@ -86,7 +86,10 @@
 
 instance ToJSON Parent where
   toJSON (DatabaseParent dbId) = object ["type" .= ("database_id" :: Text), "database_id" .= dbId]
-  toJSON (DataSourceParent dsId) = object ["type" .= ("data_source_id" :: Text), "data_source_id" .= dsId]
+  toJSON (DataSourceParent dsId mDbId) =
+    object $
+      ["type" .= ("data_source_id" :: Text), "data_source_id" .= dsId]
+        <> maybe [] (\dbId -> ["database_id" .= dbId]) mDbId
   toJSON (PageParent pId) = object ["type" .= ("page_id" :: Text), "page_id" .= pId]
   toJSON (BlockParent bId) = object ["type" .= ("block_id" :: Text), "block_id" .= bId]
   toJSON (WorkspaceParent ws) = object ["type" .= ("workspace" :: Text), "workspace" .= ws]
diff --git a/src/Notion/V1/Filter.hs b/src/Notion/V1/Filter.hs
--- a/src/Notion/V1/Filter.hs
+++ b/src/Notion/V1/Filter.hs
@@ -237,6 +237,8 @@
   | DateNextMonth
   | DateNextYear
   | DateThisWeek
+  | DateThisMonth
+  | DateThisYear
   | DatePastWeek
   | DatePastMonth
   | DatePastYear
@@ -255,6 +257,8 @@
   DateNextMonth -> Aeson.object ["next_month" .= Aeson.object []]
   DateNextYear -> Aeson.object ["next_year" .= Aeson.object []]
   DateThisWeek -> Aeson.object ["this_week" .= Aeson.object []]
+  DateThisMonth -> Aeson.object ["this_month" .= Aeson.object []]
+  DateThisYear -> Aeson.object ["this_year" .= Aeson.object []]
   DatePastWeek -> Aeson.object ["past_week" .= Aeson.object []]
   DatePastMonth -> Aeson.object ["past_month" .= Aeson.object []]
   DatePastYear -> Aeson.object ["past_year" .= Aeson.object []]
diff --git a/src/Notion/V1/Pages.hs b/src/Notion/V1/Pages.hs
--- a/src/Notion/V1/Pages.hs
+++ b/src/Notion/V1/Pages.hs
@@ -43,6 +43,7 @@
 import Notion.V1.ListOf (ListOf)
 import Notion.V1.PropertyValue (PropertyValue)
 import Notion.V1.Users (UserReference)
+import Servant.API (QueryParams)
 
 -- | Page ID
 type PageID = UUID
@@ -58,6 +59,8 @@
     icon :: Maybe Icon,
     parent :: Parent,
     inTrash :: Bool,
+    isLocked :: Maybe Bool,
+    isArchived :: Maybe Bool,
     properties :: Map Text PropertyValue,
     url :: Text,
     publicUrl :: Maybe Text,
@@ -79,6 +82,8 @@
       icon <- o .:? "icon"
       parent <- o .: "parent"
       inTrash <- (o .: "in_trash") <|> (o .: "is_archived") <|> (o .: "archived") <|> pure False
+      isLocked <- o .:? "is_locked"
+      isArchived <- o .:? "is_archived"
       properties <- o .: "properties"
       url <- o .: "url"
       publicUrl <- o .:? "public_url"
@@ -102,6 +107,8 @@
         "url" .= url,
         "object" .= object
       ]
+        <> maybe [] (\v -> ["is_locked" .= v]) isLocked
+        <> maybe [] (\v -> ["is_archived" .= v]) isArchived
         <> maybe [] (\pu -> ["public_url" .= pu]) publicUrl
 
 -- | Template configuration for page creation and updates.
@@ -167,6 +174,8 @@
 data UpdatePage = UpdatePage
   { properties :: PageProperties,
     inTrash :: Maybe Bool,
+    isLocked :: Maybe Bool,
+    isArchived :: Maybe Bool,
     icon :: Maybe Icon,
     cover :: Maybe Cover,
     template :: Maybe Template,
@@ -183,6 +192,8 @@
   UpdatePage
     { properties,
       inTrash = Nothing,
+      isLocked = Nothing,
+      isArchived = Nothing,
       icon = Nothing,
       cover = Nothing,
       template = Nothing,
@@ -340,6 +351,7 @@
 type API =
   "pages"
     :> ( Capture "page_id" PageID
+           :> QueryParams "filter_properties" Text
            :> Get '[JSON] PageObject
            :<|> ReqBody '[JSON] CreatePage
            :> Post '[JSON] PageObject
diff --git a/src/Notion/V1/Properties.hs b/src/Notion/V1/Properties.hs
--- a/src/Notion/V1/Properties.hs
+++ b/src/Notion/V1/Properties.hs
@@ -253,6 +253,9 @@
   | Count
   | Empty
   | NotEmpty
+  | CountPerGroup
+  | PercentPerGroup
+  | Unique
   deriving stock (Eq, Show, Generic)
 
 instance FromJSON RollupFunction where
@@ -282,6 +285,9 @@
     "count" -> pure Count
     "empty" -> pure Empty
     "not_empty" -> pure NotEmpty
+    "count_per_group" -> pure CountPerGroup
+    "percent_per_group" -> pure PercentPerGroup
+    "unique" -> pure Unique
     other -> fail $ "Unknown RollupFunction: " <> unpack other
 
 instance ToJSON RollupFunction where
@@ -310,6 +316,9 @@
   toJSON Count = Aeson.String "count"
   toJSON Empty = Aeson.String "empty"
   toJSON NotEmpty = Aeson.String "not_empty"
+  toJSON CountPerGroup = Aeson.String "count_per_group"
+  toJSON PercentPerGroup = Aeson.String "percent_per_group"
+  toJSON Unique = Aeson.String "unique"
 
 -- | Relation property type configuration.
 data RelationType
diff --git a/src/Notion/V1/RichText.hs b/src/Notion/V1/RichText.hs
--- a/src/Notion/V1/RichText.hs
+++ b/src/Notion/V1/RichText.hs
@@ -83,6 +83,8 @@
   | DatabaseMention {database :: UUID}
   | DateMention {date :: Date}
   | LinkPreviewMention {url :: Text}
+  | TemplateMentionDate {templateMentionDate :: Text}
+  | TemplateMentionUser {templateMentionUser :: Text}
   deriving stock (Eq, Generic, Show)
 
 instance FromJSON MentionContent where
@@ -103,6 +105,13 @@
         "link_preview" -> do
           lpObj <- o .: "link_preview"
           LinkPreviewMention <$> parseUrlField lpObj
+        "template_mention" -> do
+          tmObj <- o .: "template_mention"
+          tmType <- tmObj .: "type"
+          case tmType of
+            "template_mention_date" -> TemplateMentionDate <$> tmObj .: "template_mention_date"
+            "template_mention_user" -> TemplateMentionUser <$> tmObj .: "template_mention_user"
+            other2 -> fail $ "Unknown template_mention type: " <> unpack other2
         other -> fail $ "Unknown mention type: " <> unpack other
     _ -> fail "Expected object for MentionContent"
     where
@@ -125,6 +134,16 @@
       object ["type" .= ("date" :: Text), "date" .= d]
     LinkPreviewMention u ->
       object ["type" .= ("link_preview" :: Text), "link_preview" .= object ["url" .= u]]
+    TemplateMentionDate d ->
+      object
+        [ "type" .= ("template_mention" :: Text),
+          "template_mention" .= object ["type" .= ("template_mention_date" :: Text), "template_mention_date" .= d]
+        ]
+    TemplateMentionUser u ->
+      object
+        [ "type" .= ("template_mention" :: Text),
+          "template_mention" .= object ["type" .= ("template_mention_user" :: Text), "template_mention_user" .= u]
+        ]
 
 -- | Equation content
 newtype EquationContent = EquationContent
diff --git a/src/Notion/V1/Users.hs b/src/Notion/V1/Users.hs
--- a/src/Notion/V1/Users.hs
+++ b/src/Notion/V1/Users.hs
@@ -8,6 +8,7 @@
     UserType (..),
     PersonUser (..),
     BotUser (..),
+    WorkspaceLimits (..),
     UserReference (..),
 
     -- * Servant
@@ -56,10 +57,21 @@
 instance FromJSON PersonUser where
   parseJSON = genericParseJSON aesonOptions
 
+-- | Workspace limits for bot users.
+data WorkspaceLimits = WorkspaceLimits
+  { maxFileUploadSizeInBytes :: Maybe Natural
+  }
+  deriving stock (Generic, Show)
+
+instance FromJSON WorkspaceLimits where
+  parseJSON = genericParseJSON aesonOptions
+
 -- | Bot user
 data BotUser = BotUser
   { owner :: Maybe UserOwner,
-    workspaceName :: Maybe Text
+    workspaceName :: Maybe Text,
+    workspaceId :: Maybe Text,
+    workspaceLimits :: Maybe WorkspaceLimits
   }
   deriving stock (Generic, Show)
 
diff --git a/tasty/Main.hs b/tasty/Main.hs
--- a/tasty/Main.hs
+++ b/tasty/Main.hs
@@ -12,7 +12,7 @@
 import Notion.V1.BlockContent (BlockContent (..), CodeLanguage (..), FileSource (..), SyncedFrom (..), blockContentType, bookmarkBlock, bulletedListItemBlock, calloutBlock, codeBlock, dividerBlock, headingBlock, imageBlock, mkRichText, numberedListItemBlock, paragraphBlock, quoteBlock, textBlock, toDoBlock, toggleBlock, withChildren)
 import Notion.V1.Blocks (AppendBlockChildren (..), BlockObject (..), Position (..))
 import Notion.V1.Blocks qualified as Blocks
-import Notion.V1.Comments (CommentObject (..), CreateComment (..))
+import Notion.V1.Comments (CommentAttachment (..), CommentDisplayName (..), CommentObject (..), CreateComment (..))
 import Notion.V1.Comments qualified as Comments
 import Notion.V1.Common (Color (..), Cover (..), ExternalFile (..), Icon (..), Parent (..), UUID (..))
 import Notion.V1.CustomEmojis (CustomEmoji (..))
@@ -42,10 +42,10 @@
 import Notion.V1.Pagination (PaginationResult (..), paginateCollect)
 import Notion.V1.Properties qualified as Props
 import Notion.V1.PropertyValue qualified as PV
-import Notion.V1.RichText (Annotations (..), Date (..), RichText (..), RichTextContent (..), TextContent (..), defaultAnnotations)
+import Notion.V1.RichText (Annotations (..), Date (..), MentionContent (..), RichText (..), RichTextContent (..), TextContent (..), defaultAnnotations)
 import Notion.V1.RichText qualified as RT
 import Notion.V1.Search (SearchRequest (..), SearchResult (..), dataSourceFilter, pageFilter, parseSearchResults)
-import Notion.V1.Users (UserObject (..))
+import Notion.V1.Users (BotUser (..), UserObject (..), WorkspaceLimits (..))
 import Notion.V1.Views (CreateView (..), QueryView (..), UpdateView (..), ViewObject (..), ViewType (..))
 import System.Environment qualified as Environment
 import Test.Tasty
@@ -208,6 +208,8 @@
         UpdatePage
           { properties = Map.empty,
             inTrash = Just True,
+            isLocked = Nothing,
+            isArchived = Nothing,
             icon = Nothing,
             cover = Nothing,
             template = Nothing,
@@ -265,7 +267,17 @@
       testCase "BlockContent: blocks with children include children key" testBlockContentHasChildrenKey,
       testCase "BlockUpdate with nested children" testBlockUpdateWithChildren,
       testCase "BlockContent: parse from raw JSON with children" testParseBlockContentWithChildren,
-      testCase "BlockUpdate omits type key" testBlockUpdateSerialization
+      testCase "BlockUpdate omits type key" testBlockUpdateSerialization,
+      testCase "BlockContent round-trip: heading_4" testBlockContentHeading4,
+      testCase "BlockContent round-trip: tab" testBlockContentTab,
+      testCase "BlockContent parse: meeting_notes" testBlockContentMeetingNotes,
+      testCase "BlockContent round-trip: template" testBlockContentTemplate,
+      testCase "RichText parse: template_mention_date" testTemplateMentionDate,
+      testCase "RichText round-trip: template_mention_user" testTemplateMentionUser,
+      testCase "PageObject parse: isLocked and isArchived" testPageObjectLockedArchived,
+      testCase "Parent parse: DataSourceParent with databaseId" testDataSourceParentWithDbId,
+      testCase "BlockContent round-trip: column with widthRatio" testColumnWidthRatio,
+      testCase "BotUser parse: workspaceId and workspaceLimits" testBotUserFields
     ]
 
 testParseBlockObject :: Assertion
@@ -426,8 +438,8 @@
     ColumnListBlock
       { children =
           Vector.fromList
-            [ ColumnBlock {children = Vector.singleton (textBlock "Column 1 content")},
-              ColumnBlock {children = Vector.singleton (textBlock "Column 2 content")}
+            [ ColumnBlock {widthRatio = Nothing, children = Vector.singleton (textBlock "Column 1 content")},
+              ColumnBlock {widthRatio = Nothing, children = Vector.singleton (textBlock "Column 2 content")}
             ]
       }
 
@@ -658,7 +670,13 @@
       testCase "Sort: property ascending" testSortProperty,
       testCase "Sort: timestamp descending" testSortTimestamp,
       testCase "UpdateDataSource nullable property deletion" testSerializeNullablePropertyDeletion,
-      testCase "paginateAll collects all pages" testPaginateAll
+      testCase "paginateAll collects all pages" testPaginateAll,
+      testCase "CreateComment with attachments and displayName" testSerializeCreateComment,
+      testCase "CommentAttachment decodes read-side shape" testDeserializeCommentAttachmentReadShape,
+      testCase "CommentDisplayName captures resolved_name" testDeserializeCommentDisplayNameResolvedName,
+      testCase "UpdatePage with isLocked and isArchived" testSerializeUpdatePageLockArchive,
+      testCase "RollupFunction round-trip: new variants" testRollupFunctionNewVariants,
+      testCase "DateCondition: this_month and this_year" testDateConditionThisMonthYear
     ]
 
 testSerializeAppendNoPosition :: Assertion
@@ -916,6 +934,8 @@
         UpdatePage
           { properties = Map.empty,
             inTrash = Nothing,
+            isLocked = Nothing,
+            isArchived = Nothing,
             icon = Nothing,
             cover = Nothing,
             template = Just (DefaultTemplate (Just "America/Chicago")),
@@ -1132,7 +1152,9 @@
         CreateComment
           { parent = PageParent {pageId},
             richText = Vector.singleton (mkTypedRichText "This is a page-level comment from E2E tests."),
-            discussionId = Nothing
+            discussionId = Nothing,
+            attachments = Nothing,
+            displayName = Nothing
           }
   comment1 <- createComment pageComment
   let CommentObject {id = comment1Id} = comment1
@@ -1143,7 +1165,9 @@
         CreateComment
           { parent = BlockParent {blockId},
             richText = Vector.singleton (mkTypedRichText "This is a block-level comment from E2E tests."),
-            discussionId = Nothing
+            discussionId = Nothing,
+            attachments = Nothing,
+            displayName = Nothing
           }
   comment2 <- createComment blockComment
   let CommentObject {id = comment2Id} = comment2
@@ -1230,7 +1254,7 @@
   -- Create a page in the database's data source
   let titleRt = mkPlainRichText "Database Markdown E2E"
       props = Map.fromList [("title", PV.titleValue (Vector.singleton titleRt))]
-      req = mkCreatePage (DataSourceParent {dataSourceId = dsId}) props
+      req = mkCreatePage (DataSourceParent {dataSourceId = dsId, parentDatabaseId = Nothing}) props
   page <- createPage req
   let PageObject {id = pageId} = page
 
@@ -1486,6 +1510,227 @@
             Just (Aeson.Object _) -> pure ()
             _ -> assertFailure "Expected NewColumn to be an object"
         _ -> assertFailure "Expected properties object"
+    _ -> assertFailure "Expected JSON object"
+
+-- =====================================================================
+-- New type tests (Milestone 6)
+-- =====================================================================
+
+testBlockContentHeading4 :: Assertion
+testBlockContentHeading4 = roundTrip $ Heading4Block (mkRichText "H4 Title") Default False Vector.empty
+
+testBlockContentTab :: Assertion
+testBlockContentTab = roundTrip $ TabBlock {children = Vector.singleton (textBlock "Tab content")}
+
+testBlockContentMeetingNotes :: Assertion
+testBlockContentMeetingNotes = do
+  let json =
+        "{\"type\":\"meeting_notes\",\"meeting_notes\":"
+          <> "{\"title\":\"Weekly Sync\",\"status\":\"scheduled\","
+          <> "\"calendar_event\":null,\"recording\":null}}"
+  case Aeson.eitherDecode json of
+    Left err -> assertFailure $ "Failed to parse meeting_notes: " <> err
+    Right (MeetingNotesBlock {meetingTitle}) ->
+      assertEqual "meetingTitle" "Weekly Sync" meetingTitle
+    Right other -> assertFailure $ "Expected MeetingNotesBlock, got: " <> show other
+
+testBlockContentTemplate :: Assertion
+testBlockContentTemplate =
+  roundTrip $
+    TemplateBlock
+      { richText = mkRichText "Template text",
+        children = Vector.singleton (textBlock "Template child")
+      }
+
+testTemplateMentionDate :: Assertion
+testTemplateMentionDate = do
+  let json =
+        "{\"type\":\"mention\",\"mention\":"
+          <> "{\"type\":\"template_mention\",\"template_mention\":"
+          <> "{\"type\":\"template_mention_date\",\"template_mention_date\":\"today\"}}"
+          <> ",\"plain_text\":\"@today\",\"annotations\":"
+          <> "{\"bold\":false,\"italic\":false,\"strikethrough\":false,"
+          <> "\"underline\":false,\"code\":false,\"color\":\"default\"}}"
+  case Aeson.eitherDecode json of
+    Left err -> assertFailure $ "Failed to parse template_mention: " <> err
+    Right RichText {content = MentionContentWrapper (TemplateMentionDate d)} ->
+      assertEqual "template_mention_date" "today" d
+    Right other -> assertFailure $ "Expected TemplateMentionDate, got: " <> show other
+
+testTemplateMentionUser :: Assertion
+testTemplateMentionUser = do
+  let mc = TemplateMentionUser "me"
+      json = Aeson.toJSON mc
+  case Aeson.fromJSON json of
+    Aeson.Success decoded -> assertEqual "round-trip" mc decoded
+    Aeson.Error err -> assertFailure $ "Round-trip failed: " <> err
+
+testPageObjectLockedArchived :: Assertion
+testPageObjectLockedArchived = do
+  let json =
+        "{\"object\":\"page\",\"id\":\"page-1\""
+          <> ",\"created_time\":\"2025-10-01T12:00:00.000+00:00\""
+          <> ",\"last_edited_time\":\"2025-10-01T12:30:00.000+00:00\""
+          <> ",\"created_by\":{\"object\":\"user\",\"id\":\"u-1\"}"
+          <> ",\"last_edited_by\":{\"object\":\"user\",\"id\":\"u-2\"}"
+          <> ",\"parent\":{\"type\":\"page_id\",\"page_id\":\"parent-1\"}"
+          <> ",\"in_trash\":false"
+          <> ",\"is_locked\":true"
+          <> ",\"is_archived\":false"
+          <> ",\"properties\":{}"
+          <> ",\"url\":\"https://notion.so/page-1\"}"
+  case Aeson.eitherDecode json of
+    Left err -> assertFailure $ "Failed to parse PageObject: " <> err
+    Right (page :: PageObject) -> do
+      let PageObject {isLocked = locked, isArchived = archived} = page
+      assertEqual "isLocked" (Just True) locked
+      assertEqual "isArchived" (Just False) archived
+
+testDataSourceParentWithDbId :: Assertion
+testDataSourceParentWithDbId = do
+  let json =
+        "{\"type\":\"data_source_id\",\"data_source_id\":\"ds-1\",\"database_id\":\"db-1\"}"
+  case Aeson.eitherDecode json of
+    Left err -> assertFailure $ "Failed to parse DataSourceParent: " <> err
+    Right (DataSourceParent dsId dbId) -> do
+      assertEqual "dataSourceId" (UUID "ds-1") dsId
+      assertEqual "parentDatabaseId" (Just (UUID "db-1")) dbId
+    Right other -> assertFailure $ "Expected DataSourceParent, got: " <> show other
+
+testColumnWidthRatio :: Assertion
+testColumnWidthRatio =
+  roundTrip $
+    ColumnBlock
+      { widthRatio = Just 0.5,
+        children = Vector.singleton (textBlock "Half width")
+      }
+
+testBotUserFields :: Assertion
+testBotUserFields = do
+  let json =
+        "{\"owner\":{\"type\":\"workspace\",\"workspace\":true}"
+          <> ",\"workspace_name\":\"My Workspace\""
+          <> ",\"workspace_id\":\"ws-123\""
+          <> ",\"workspace_limits\":{\"max_file_upload_size_in_bytes\":5242880}}"
+  case Aeson.eitherDecode json of
+    Left err -> assertFailure $ "Failed to parse BotUser: " <> err
+    Right bot -> do
+      assertEqual "workspaceId" (Just "ws-123") (Notion.V1.Users.workspaceId bot)
+      case Notion.V1.Users.workspaceLimits bot of
+        Just wl -> assertEqual "maxFileUploadSizeInBytes" (Just 5242880) (maxFileUploadSizeInBytes wl)
+        Nothing -> assertFailure "Expected workspaceLimits to be present"
+
+testSerializeCreateComment :: Assertion
+testSerializeCreateComment = do
+  let req =
+        CreateComment
+          { parent = PageParent {pageId = UUID "p-1"},
+            richText = Vector.singleton (mkPlainRichText "Hello"),
+            discussionId = Nothing,
+            attachments =
+              Just
+                ( Vector.singleton
+                    CommentAttachment
+                      { name = Just "file.pdf",
+                        type_ = Just "external",
+                        category = Nothing,
+                        external = Just (ExternalFile {url = "https://example.com/file.pdf"}),
+                        file = Nothing
+                      }
+                ),
+            displayName =
+              Just
+                CommentDisplayName
+                  { type_ = "user",
+                    emoji = Just "🎉",
+                    displayName = Just "Bot",
+                    resolvedName = Nothing
+                  }
+          }
+      json = Aeson.toJSON req
+  case json of
+    Aeson.Object o -> do
+      assertBool "should have attachments" (KeyMap.member "attachments" o)
+      assertBool "should have display_name" (KeyMap.member "display_name" o)
+    _ -> assertFailure "Expected JSON object"
+
+testDeserializeCommentAttachmentReadShape :: Assertion
+testDeserializeCommentAttachmentReadShape = do
+  let json =
+        "{\"category\":\"image\""
+          <> ",\"file\":{\"url\":\"https://example.com/img.png\""
+          <> ",\"expiry_time\":\"2026-04-16T15:53:52.000Z\"}}"
+  case Aeson.eitherDecode json :: Either String CommentAttachment of
+    Left err -> assertFailure $ "Failed to parse CommentAttachment: " <> err
+    Right CommentAttachment {name, type_, category, file} -> do
+      assertEqual "category" (Just "image") category
+      assertEqual "name absent" Nothing name
+      assertEqual "type_ absent" Nothing type_
+      case file of
+        Just _ -> pure ()
+        Nothing -> assertFailure "Expected file to be Just"
+
+testDeserializeCommentDisplayNameResolvedName :: Assertion
+testDeserializeCommentDisplayNameResolvedName = do
+  let json = "{\"type\":\"user\",\"resolved_name\":\"Tanaka Hiroshi\"}"
+  case Aeson.eitherDecode json :: Either String CommentDisplayName of
+    Left err -> assertFailure $ "Failed to parse CommentDisplayName: " <> err
+    Right CommentDisplayName {type_, displayName, resolvedName} -> do
+      assertEqual "type_" "user" type_
+      assertEqual "resolvedName" (Just "Tanaka Hiroshi") resolvedName
+      assertEqual "displayName absent" Nothing displayName
+
+testSerializeUpdatePageLockArchive :: Assertion
+testSerializeUpdatePageLockArchive = do
+  let req =
+        UpdatePage
+          { properties = Map.empty,
+            inTrash = Nothing,
+            isLocked = Just True,
+            isArchived = Just False,
+            icon = Nothing,
+            cover = Nothing,
+            template = Nothing,
+            eraseContent = Nothing
+          }
+      json = Aeson.toJSON req
+  case json of
+    Aeson.Object o -> do
+      assertEqual "is_locked" (Just (Aeson.Bool True)) (KeyMap.lookup "is_locked" o)
+      assertEqual "is_archived" (Just (Aeson.Bool False)) (KeyMap.lookup "is_archived" o)
+    _ -> assertFailure "Expected JSON object"
+
+testRollupFunctionNewVariants :: Assertion
+testRollupFunctionNewVariants = do
+  let variants = [Props.CountPerGroup, Props.PercentPerGroup, Props.Unique]
+  mapM_
+    ( \v -> do
+        let json = Aeson.toJSON v
+        case Aeson.fromJSON json of
+          Aeson.Success decoded ->
+            assertEqual ("round-trip for " <> show v) v decoded
+          Aeson.Error err ->
+            assertFailure $ "Failed to decode " <> show v <> ": " <> err
+    )
+    variants
+
+testDateConditionThisMonthYear :: Assertion
+testDateConditionThisMonthYear = do
+  let thisMonth = Aeson.toJSON (F.TimestampFilter F.FilterCreatedTime F.DateThisMonth)
+  case thisMonth of
+    Aeson.Object o ->
+      case KeyMap.lookup "created_time" o of
+        Just (Aeson.Object inner) ->
+          assertBool "should have this_month" (KeyMap.member "this_month" inner)
+        _ -> assertFailure "Expected created_time object"
+    _ -> assertFailure "Expected JSON object"
+  let thisYear = Aeson.toJSON (F.TimestampFilter F.FilterLastEditedTime F.DateThisYear)
+  case thisYear of
+    Aeson.Object o ->
+      case KeyMap.lookup "last_edited_time" o of
+        Just (Aeson.Object inner) ->
+          assertBool "should have this_year" (KeyMap.member "this_year" inner)
+        _ -> assertFailure "Expected last_edited_time object"
     _ -> assertFailure "Expected JSON object"
 
 -- =====================================================================
