Skip to content
English
  • There are no suggestions because the search field is empty.

Mapping Page Data: Mapping Recipes

This guide is part of Mapping Page Data into Toggl.

Every construct the mapping format supports, with a snippet and what it produces. 

Reading a value off the page

A resolver starts somewhere, then travels. Pick one starting point and one way to travel — the editor rejects a resolver that names two of either.

Starting points

Key Starts at Use when
root The whole document One button on the page, value lives anywhere
closest Nearest matching ancestor of the button One button per row or card — keeps each button reading its own row
parent N levels up from the button The value sits in a wrapper with no useful class or test id


⚠️ On a list or board, root is almost always wrong. Every button would read the same value — whichever row happens to be first. Use closest scoped to the row.

Ways to travel

"title":  { "closest": "@card", "query": "h3.title" }  "after":  { "closest": ".me", "nextSibling": 1 }  "before": { "closest": ".me", "previousSibling": 1 } 

query is a CSS selector. The sibling forms step to the element beside your starting point instead — useful when a value has no selector of its own but sits predictably next to something that does.

Read an attribute instead of the text

"ticketId": { "closest": ".card", "textAttribute": "data-id" } 

Produces AB-9 from <div class="card" data-id="AB-9">. This is how the Zendesk integration gets the ticket number — it is in an attribute, never shown on screen.

Strip markdown

"clean": { "query": ".body", "unmarkdown": true } 

**bold** text becomes bold text.


Reading from the URL

Often more reliable than the page, because URLs change far less often than markup.

"repo": { "url": "path",  "regex": "^/([^/]+/[^/]+)", "replace": "$1" } "qid":  { "url": "query", "regex": "id=([0-9]+)" } 

url accepts path, query, hash or href.


Shaping the value with regex

Goal Write Result
Keep only part of it "regex": "#([0-9]+)" 23450 from #23450
Add a prefix "regex": "^(.*)$", "replace": "State:$1" State:Open
Drop a prefix "regex": "^.*/(.*)$", "replace": "$1" Sanjin from Support/Sanjin
Fixed text on a match "regex": "/issues/", "replace": "Kind:Issue" Kind:Issue
Two pieces "regex": "^/([^/]+)/([^/]+)", "replace": "$1 :: $2" vitejs :: vite

Omit replace and it defaults to $1. If the regex matches nothing you get no value at all — never a half-formed one.


Combining values

These apply to description, task and project. Tags behave differently — see the next section.

Glue several pieces together

"description": { "resolvers": ["key", " ", "title"] } 

Produces AB-9 Fix the thing. Any name that is not a resolver is treated as literal text — that is how the " " becomes a space, and how "#" or ": " become separators.

Join every match with a separator

"description": { "join": ", ", "resolvers": "label" } 

With two labels on the page, produces bug, urgent.

Try one, then another

"project": [   { "resolvers": ["breadcrumbProject"] },   { "resolvers": ["boardHeaderProject"] } ] 

A list of locators is a fallback chain: the first one that produces something wins, the rest are skipped. Jira uses exactly this, because the project name sits in one of two places depending on whether the issue is a full page or a modal over a board.


Tags

Tags are the exception to everything above, in two ways.

They union, they do not fall back

"tags": ["labelTag", "stateTag", "authorTag"] 

All three contribute. Every other field would stop at the first. This is what lets one button collect tags from several unrelated places on the page.

One resolver can produce many tags

A resolver matching three labels produces three tags, and regex/replace is applied to each one:

"labelTag": { "query": ".label", "regex": "^(.*)$", "replace": "Label:$1" } 

Gives Label:bug and Label:urgent, not one merged value.

field:value needs regex, not literals


🚨 Writing ["Status:", "status"] under tags produces two tagsStatus: and To Do — because every part becomes its own tag. join does not help; it is ignored for tags.

Build the label into the value with regex and replace, as in the example above.

Duplicates are removed automatically, case-insensitively, so a value that appears twice on the page still produces one tag. A start sends at most 10 tags; anything beyond that is dropped.


Naming things

Selector aliases

Define a selector once and reuse it with @name:

"selectors": { "card": "[data-testid='board-card']" }, "resolvers": {   "title": { "closest": "@card", "query": ".title" } } 

A stable identity for the item

"taskResolver": { "keyPattern": "^[A-Za-z][A-Za-z0-9]+-[0-9]+$" } 

A regex tried against the first word of the description. A match becomes the item's identity, which is what keeps the button attached to its running timer when someone renames the ticket.


The trap that costs the most time


🕵️ A misspelled resolver name becomes literal text, and it wins.

In a fallback chain, [{ "resolvers": ["titel"] }, { "resolvers": ["title"] }] produces the literal string titel. The first candidate "succeeded", so the real one is never tried.

The editor does not catch this: a bare word is valid JSON. The button does. With the side panel open, its data-toggl-debug-issues attribute names the unknown resolver, so check it before assuming the selector is wrong.