How to Map Notion Properties to TableCrafter Columns

Updated July 2026 • 7 min read • By Fahad Murtaza

TableCrafter table builder, connect Gravity Forms, Google Sheets, Airtable, CSV, or JSON data sources
TableCrafter table builder, connect Gravity Forms, Google Sheets, Airtable, CSV, or JSON data sources

Notion supports more than a dozen property types, and each one is structured differently in the Notion API response. TableCrafter normalizes, "flattens", every property to a clean table value: titles become text, selects become badges, multi-selects become multiple badges, dates respect your format, and complex nested values are reduced to readable strings. This guide covers exactly how each Notion property type maps to a TableCrafter column. WordPress powers 43% of all websites globally (W3Techs, July 2026), and TableCrafter bridges the gap between the data you collect and the tables your users need to see, no custom PHP, no dashboard access required for viewers. The free version on WordPress.org supports CSV, JSON, Google Sheets, and Excel. Pro adds Gravity Forms, Airtable, Notion, WooCommerce, REST APIs, inline cell editing, export to CSV/PDF, role-based column visibility, and auto-refresh. Every table embeds on any page with a [tablecrafter] shortcode or the native Gutenberg block. Auto-refresh tables reduce page reload frequency by 89% in live-data dashboard deployments (Kinsta performance benchmarks, 2024).

How Does Property Mapping Work?

When TableCrafter queries your Notion database, the API returns each row as a set of properties, where every property is a typed object, not a plain value. A Notion select, for example, comes back as a nested object containing the option's name and color, not just the text. TableCrafter's payload normalizer walks each property, reads its type, and reduces it to a single scalar (or a comma-joined string for multi-value types) that a table cell can display.

Each property name in your database becomes a column key. You can then toggle columns on or off, relabel them, set their display type, width, and sort/filter behavior in the Columns configuration panel.

The normalizer has dedicated rules for 16 Notion property types: title, rich_text, number, checkbox, select, status, multi_select, date, email, url, phone_number, people, files, relation, created_time, and last_edited_time. Any property type not on that list falls through to a JSON-encoding fallback so that data is never silently discarded from the table.

Your Notion integration token is stored encrypted in the WordPress database using the same credential service that handles Airtable authentication. The token is decrypted server-side only at fetch time and is never exposed to the browser. Fetched rows are cached as a WordPress transient; the cache duration is configurable per table with a minimum of 5 minutes, a maximum of 24 hours, and a default of 30 minutes.

The normalizer never silently drops data. Any property type without a dedicated rule is JSON-encoded into the cell rather than discarded, so you always see something and can adjust the column. Notion is a Pro-tier data source; the free version on WordPress.org supports CSV, JSON, Google Sheets, and Excel.

How Does Three Columns TableCrafter Adds Automatically Work?

Alongside the properties defined in your Notion database schema, TableCrafter injects three metadata fields from the top-level Notion row object. These come from the id, created_time, and last_edited_time keys that Notion includes on every page result, not from any property you defined. They appear as columns in the Columns panel under the key names below, where you can toggle them on or off, relabel them, or reorder them exactly like any data property.

These three columns are always available regardless of which properties your Notion database exposes. If your database also has explicit created_time or last_edited_time properties added through the Notion UI, you will see both the auto-injected metadata and the property-level versions in the Columns panel. Hide the duplicate in column settings to avoid showing the same timestamp twice on the same row.

How Does Property Type Reference Work?

Title

Notion type: title

Every Notion database has exactly one title property. The API returns it as an array of rich-text segment objects: [{"plain_text": "Q3 Roadmap", "type": "text", "annotations": {...}}, ...]. Each segment carries a plain_text field alongside styling metadata such as bold, italic, color, and link annotations. TableCrafter's normalizer iterates every segment, concatenates the plain_text values in order, and returns a single clean string. All formatting metadata is discarded; the cell contains readable text, not nested JSON.

In the table, the title renders as left-aligned plain text and is the primary candidate for the search box. The "Text contains" filter evaluates this column along with any other enabled text columns. Sorting is lexicographic (A-Z / Z-A). If you need numeric ordering for titles like "Item 9" before "Item 10", store the sort order in a separate Number property instead.

Practical gotcha: Titles that contain Notion inline mentions or equation blocks have those segments silently skipped because they carry no plain_text value. Plain typed text is always preserved. When write-back is enabled (Pro inline editing), TableCrafter sends the edited cell value back to Notion as a single plain-text segment, which is the standard format the Notion API expects for title properties.

Rich Text

Notion type: rich_text

The API returns a rich-text property in the same array-of-segments format as the title type: [{"plain_text": "Hello world", "annotations": {"bold": true}, ...}, ...]. TableCrafter's normalizer concatenates each segment's plain_text value and discards all annotation metadata. Bold text, italic text, colored highlights, and inline hyperlinks are stripped; the cell shows only the readable character sequence.

This is the correct behavior for most use cases: a notes or description column should surface its text content, not HTML markup. However, if your rich-text property stores URLs that matter to visitors, move those URLs to a dedicated Notion url property and map that column as a URL type so that they render as clickable links.

Practical gotcha: Notion's rich-text database property does not transmit paragraph break characters between blocks; the API flattens everything into a continuous sequence. Very long values appear as a single unbroken paragraph in the table cell. Set a fixed column width or enable cell truncation in the Columns panel to keep the table scannable when note fields are long. The text-contains filter and search both work against the full concatenated string.

Number

Notion type: number

The Notion API returns a number property as {"type": "number", "number": 42.5} where the value is a JSON numeric literal. TableCrafter's normalizer reads that value directly and returns it as a PHP integer or float, preserving its type. Table sorting is therefore numeric rather than lexicographic, so "9" sorts before "10" without any configuration.

An empty Notion number cell sends {"number": null}. The normalizer returns an empty string in that case so the cell appears blank; rows with no value sort to the bottom in ascending order and to the top in descending order. A currency prefix or unit suffix can be added in the Columns panel under the number format settings without affecting sort or filter behavior.

Practical gotcha: Notion stores the raw numeric value in the API regardless of how the property is formatted in the Notion UI. A property configured as a percentage in Notion sends 0.25 for a 25% value, not 25. A currency property sends the raw number without a symbol. You need to set the display prefix or suffix in TableCrafter's Columns panel to match the formatting your visitors expect.

Select

Notion type: select

The Notion API returns a select property as a nested object containing the chosen option's id, name, and color: {"select": {"id": "...", "name": "In Progress", "color": "blue"}}. TableCrafter's normalizer reads only the name field and returns it as a plain string. The color that Notion stores for the option is not surfaced in the table, so "In Progress" appears as a badge in TableCrafter's default badge styling rather than Notion's blue tint.

The filter builds a dropdown from all distinct option names present in the fetched rows. A visitor selects one value and only rows with that exact option appear. Sorting is lexicographic on the option name.

Practical gotcha: If a row has no option selected, Notion returns {"select": null}. The normalizer returns an empty string, and those rows appear with a blank badge cell. They do not appear in the filter dropdown because there is no value to list, but they remain in the table. When write-back is enabled (Pro inline editing), selecting a value sends {"select": {"name": "value"}} back to Notion. To clear a select field, send an empty value and Notion will accept it as null.

Status

Notion type: status

Notion's Status property has its own API type (status) distinct from select, even though the two appear similar in the Notion UI. The API returns the current status as a nested object with id, name, and color: {"status": {"name": "Done", "color": "green"}}. TableCrafter's normalizer reads only the name field and returns it as a plain string, identical to how it handles a select option. The Notion color data is not surfaced in the rendered badge.

Status columns are well-suited for task lists, project roadmaps, and content pipelines where visitors filter by stage. The filter dropdown is populated from all distinct status names in the fetched rows. Sorting is lexicographic on the name, so if you need a meaningful sort order, prefix option names with a number such as "1. Not started", "2. In progress", and "3. Done".

Practical gotcha: Notion groups Status options into three internal categories (To-do, In progress, Complete), but those categories are absent from the API response at the property level. Only the option name is transmitted to TableCrafter. If category-level filtering matters, mirror the category as a separate Select property in Notion and map it as an additional column so visitors can filter on both the status name and the broader category.

Multi-Select

Notion type: multi_select

A multi-select property returns an array of option objects, each with an id, name, and color: [{"name": "Urgent"}, {"name": "Bug"}]. TableCrafter's normalizer iterates the array, collects each option's name, and joins them with a comma. A row tagged "Urgent" and "Bug" becomes the string "Urgent,Bug". The table renders each comma-separated value as a separate badge inside the same cell.

The filter is a multi-select dropdown populated from all distinct option names across every fetched row. A visitor can choose one or more values; a row appears if it contains any of the chosen values (OR logic). Filtering by "Bug" returns rows tagged "Bug", "Bug,Urgent", or "Feature,Bug".

Practical gotcha: Because the normalizer joins option names with a comma, any option name that itself contains a comma will cause the string to split incorrectly when the filter tries to reconstruct the individual values. This is a known limitation of the flat-string representation. If any of your multi-select options contain commas, rename them in Notion before connecting to TableCrafter to prevent filter mismatches. Option names with spaces, hyphens, or other punctuation work without issue.

Date

Notion type: date

Notion returns a date property as an object with a start key and an optional end key, both in ISO 8601 format: {"date": {"start": "2024-06-01", "end": null}} for a single date, or {"date": {"start": "2024-06-01", "end": "2024-06-07"}} for a date range. The normalizer returns the start string for point-in-time dates, or the combined string "2024-06-01 → 2024-06-07" when an end date is present.

TableCrafter formats the displayed value using the date format configured in your table settings, such as MM/DD/YYYY or DD/MM/YYYY. Sorting is chronological because ISO 8601 strings sort correctly as plain text. The filter is a date-range picker; visitors set a start and end boundary and the table shows rows whose date falls within that window.

Practical gotcha: Notion date properties support an optional time component: "2024-06-01T14:30:00.000+00:00". The normalizer returns the full ISO string including the time portion. If your format pattern omits a time specifier, the time portion is dropped from the display but sorting still works correctly because ISO strings with equal dates are then ordered by time. Date range rows display the full range string, which may not parse cleanly with a date-only format pattern.

Checkbox

Notion type: checkbox

The Notion API returns a checkbox property as {"type": "checkbox", "checkbox": true} or {"type": "checkbox", "checkbox": false}. TableCrafter's normalizer evaluates the value with PHP's !empty(), so true stays true and false (or a missing value) becomes false. The cell renders a checkmark icon for true and a dash for false.

The filter presents three options: "Yes" (only checked rows), "No" (only unchecked rows), and "Either" (no filter applied). Sorting puts checked rows first in descending order. Checkbox columns are efficient for completion status, active/inactive flags, or any simple binary field that does not need a badge label.

Practical gotcha: Notion never sends a null value for a checkbox property; an unchecked box always transmits false. This makes checkbox filtering reliable because there are no ambiguous empty-string cases to handle. When write-back is enabled (Pro inline editing), the normalizer accepts "1", "true", "yes", or "on" as truthy input strings and sends the corresponding {"checkbox": true} or {"checkbox": false} object back to the Notion API.

Email, URL, and Phone

Notion types: email, url, phone_number

Each of these three Notion property types returns a flat scalar string in the API: {"type": "email", "email": "user@example.com"}, {"type": "url", "url": "https://example.com"}, or {"type": "phone_number", "phone_number": "+1-555-123-4567"}. TableCrafter's normalizer reads the value from the matching key and returns it unchanged as a string.

TableCrafter detects the property type at column setup time and pre-configures actionable link rendering. An email column renders as a mailto: link; a phone_number column renders as a tel: link so mobile visitors can tap to call; a url column renders as a standard hyperlink. You can override the link text with a static label in the Columns panel, for example showing "Visit site" instead of the raw URL string.

Practical gotcha: Notion does not validate the content of email, URL, or phone properties, so a url field might contain a relative path or plain text without a protocol prefix. If the value does not start with https:// or http://, the rendered link will be broken. A data-quality pass in Notion to confirm all URL values are absolute is worthwhile before publishing a table that uses this column type. All three property types support the text-contains search and filter.

People

Notion type: people

A People property returns an array of Notion user objects: [{"object": "user", "id": "uuid-...", "name": "Jane Smith", "type": "person"}, ...]. TableCrafter's normalizer iterates the array, reads the name field from each user object, and joins the names with commas into a single string. If a user object has no name field -- which can happen with bot integrations or when the integration's access to user profiles is restricted -- the normalizer falls back to the user's id string for that entry, so no data is silently dropped.

The resulting cell value looks like "Jane Smith,Alex Johnson" for a row with two people assigned. The text-contains filter matches against the full joined string, so filtering by "Jane" returns all rows where any assigned person has "Jane" in their name. Sorting is lexicographic on the full concatenated string, not per person.

Practical gotcha: The Notion integration token must have permission to read user information for names to resolve. If your integration is configured with "No user information" in the Notion settings, the API returns only UUIDs. In that case every People cell displays raw UUID strings. To fix this, open the integration settings in Notion, navigate to "Capabilities", and set "User information" to "Read user information including email addresses".

Files & Media

Notion type: files

Notion file properties return an array of attachment objects. Each attachment has a name field (the display name) and either a file sub-object containing a time-limited Notion-hosted download URL, or an external sub-object for a third-party hosted link. TableCrafter's normalizer reads only the name field from each attachment and joins them with commas. A row with three attachments renders as "report.pdf,budget.xlsx,notes.txt".

The cell is searchable with a text-contains filter so visitors can locate rows by file name. Sorting is lexicographic on the full joined string. The table does not render inline thumbnails or clickable download links directly from this column type; file names are surfaced for reference only.

Practical gotcha: Notion-hosted file download URLs expire after one hour. Even if TableCrafter cached those URLs from the API response, they would be broken for any visitor loading the table more than an hour after the cache was populated. This is why the normalizer captures file names only and discards the expiring URL. To surface a working download link, upload the file to a permanent external location such as S3, Cloudflare R2, or a Google Drive public share, paste that permanent URL into a Notion URL property on the same row, and map that URL column as a link type in TableCrafter.

If you need clickable thumbnails or downloads, store a public file URL in a separate URL property and map that column as a link instead, the files property surfaces names, not URLs.

Relation

Notion type: relation

A relation property links the current row to pages in another Notion database. The API returns an array of objects where each entry contains only the id of the related page: [{"id": "a1b2c3d4-..."}, {"id": "e5f6a7b8-..."}]. TableCrafter's normalizer iterates the array and joins the page IDs with commas. A row linked to two related pages renders as a pair of UUIDs separated by a comma.

Unlike a spreadsheet VLOOKUP, the normalizer does not make a second API call to resolve each related page ID to its title or any other field. The cell value is the raw ID string, which is stable but not human-readable. Filtering and sorting treat the joined string as plain text, which has limited practical value for end users.

Practical gotcha: To display human-readable values from a related database, add a Rollup property in Notion targeting the related database and configure it to surface a specific field such as the title property. The Rollup result is a normal property that the normalizer handles as its resolved type (text, number, and so on). Map that Rollup column in TableCrafter to show visitors meaningful content instead of raw page IDs.

💡

Tip: To display human-readable related values instead of raw IDs, add a Rollup in Notion that pulls the related row's title or a key field into the current database, then map that property as a normal text column.

Created Time and Last Edited Time

Notion types: created_time, last_edited_time

These are property-level system timestamps that a Notion user has explicitly added to the database schema as visible columns. The API returns them as typed objects: {"type": "created_time", "created_time": "2024-06-01T09:15:00.000Z"} and the equivalent for last edited. TableCrafter's normalizer reads the string from the matching key and returns it as-is. No transformation is applied at this stage.

Because the raw value is an ISO 8601 string, it sorts chronologically even when treated as plain text. To display it in a readable format such as Jun 1, 2024, open the Columns panel, set the column type to Date, and select your preferred date format. The date-range filter becomes available once the column type is set to Date.

Practical gotcha: These property-level timestamps are distinct from the three auto-injected metadata columns (notion_created_time and notion_last_edited_time) that TableCrafter adds to every row regardless of your database schema. If your Notion database has these as explicit properties and the auto-injected columns are also visible, you will see the same timestamp in two columns. Hide one pair in the Columns panel to clean up the display without losing the underlying data.

Formula, Rollup, and Other Computed Types

Notion types: formula, rollup, and any newer or less common type

Formula and Rollup properties produce complex nested structures in the Notion API. A formula result might be {"type": "formula", "formula": {"type": "number", "number": 99}}; a rollup might be an array aggregation object. TableCrafter's normalizer does not have a dedicated rule for either type. The default branch in the normalizer's switch statement calls wp_json_encode() on the entire property object and returns the raw JSON string as the cell value.

This means the cell content is technically complete (no data is discarded), but it is not human-readable. A formula returning a number displays as {"type":"formula","formula":{"type":"number","number":99}} in the cell rather than simply 99. Similarly, any property type introduced by Notion after the normalizer was written will fall through to this same JSON fallback until a dedicated rule is added.

Practical gotcha: The cleanest workaround is to mirror the computed result into a simpler Notion property. A number formula result can be captured by a second formula property configured to return the same number as a plain value, which the API then transmits as a standard number type. Alternatively, a Rollup configured to "Show unique values" or aggregate to a single result maps well to a text or number column. Point the Rollup at a simple aggregation and map that property instead of the raw Rollup.

If you plan to display a Formula or Rollup column, the cleanest approach today is to mirror its result into a simple property type. For example, point a Notion formula at a number or text result and surface that, or use a Rollup configured to show a single value, then map the column normally. This avoids the raw JSON fallback.

How Does Overriding the Auto-Detected Column Type Work?

When TableCrafter first fetches your Notion database, it reads each property's type field from the API response and assigns a matching default column type. A number property becomes a numeric column with right-aligned sorting; a select becomes a badge column with a dropdown filter; a date becomes a date-formatted column with a range picker. These defaults are sensible starting points, but your actual data may call for a different display treatment.

Open the Columns panel in the TableCrafter admin and select any column to override its type, label, width, sort behavior, and visibility. The override applies only to how TableCrafter renders the normalized cell value; it does not alter the value itself or the Notion property type. Changes take effect on the next page load without re-fetching data from Notion.

Common overrides that come up when working with Notion data:

What Are the Next Steps?

Now that you understand how each Notion property type normalizes to a table cell, the next step is to configure your columns in TableCrafter and decide which properties to surface, hide, relabel, and filter. The guides below cover the full workflow from connecting your database to publishing a live table on your WordPress site.

If a column is rendering as raw JSON, refer back to the "Formula, Rollup, and Other Computed Types" section above for the recommended workaround. For questions about role-based column visibility, inline editing, or export behavior, see the related guides linked at the bottom of this page.

Frequently Asked Questions

How Does Property Mapping Work?

When TableCrafter queries your Notion database, the API returns each row as a set of properties, where every property is a typed object, not a plain value. A Notion select, for example, comes back as a nested object containing the option's name and color, not just the text. TableCrafter's payload normalizer walks each property, reads its type, and reduces it to a single scalar (or a comma-joined

What Is TableCrafter?

TableCrafter is a WordPress plugin that turns data from Gravity Forms, Google Sheets, Airtable, Notion, REST APIs, CSV files, and WooCommerce into interactive, sortable, filterable frontend tables. Embed any table on any WordPress page with the [tablecrafter] shortcode or the native Gutenberg block. No PHP or custom development required. The free version supports CSV, JSON, Google Sheets, and Excel. Pro adds Gravity Forms, Airtable, Notion, WooCommerce, REST APIs, inline cell editing, export to CSV and PDF, role-based column visibility, and auto-refresh.

Does this require PHP or developer skills?

No. TableCrafter is configured entirely through the WordPress admin interface. You choose your data source, map fields to columns, and set display preferences using point-and-click controls. Embedding uses the [tablecrafter] shortcode or the native Gutenberg block.

Is the free version sufficient or do I need Pro?

The free plugin on WordPress.org supports CSV, JSON, Google Sheets, and Excel sources with unlimited tables, rows, and columns. Pro adds Gravity Forms, Airtable, Notion, WooCommerce, REST API sources, inline cell editing, bulk row actions, export to CSV and PDF, role-based column visibility, and auto-refresh every N seconds.

Ready to try it?

TableCrafter is free on WordPress.org. Pro unlocks inline editing, role-based permissions, and advanced data sources including Notion.