8.1 KiB
Logseq DB Properties (Developer Guide)
This note explains how properties work in Logseq when using a DB graph (database mode), how that differs from a file graph (Markdown/Org), and how to use the plugin SDK APIs defined in libs/src/LSPlugin.ts.
Scope: This is written for plugin developers. It focuses on
logseq.Editor.*property APIs and tag/class modeling.
1) Two worlds: File graph vs DB graph
File graph (Markdown / Org)
- “Properties” are primarily text syntax stored in the file:
- Markdown frontmatter (
---) - Org property drawers (
:PROPERTIES:) - Inline props (
key:: value)
- Markdown frontmatter (
- When you update a property, you’re often ultimately editing text.
- SDK calls like
Editor.insertBlock/updateBlockhaveopts.properties, but note this warning in the typings:IBatchBlock.propertiesis not supported for DB graph.
DB graph (Database mode)
DB mode treats properties as first-class DB entities.
Think of it as a 2-layer model:
-
Property definitions (schema)
- A property key (e.g.
rating,authors,zotero_key) exists as an entity. - It may have a schema: type, cardinality, visibility.
- A property key (e.g.
-
Property values (data)
- Blocks/pages can store values for property keys.
- Persisted structurally in the DB (not by emitting
key:: valuetext).
In DB graphs, prefer the dedicated APIs:
- Property schema:
Editor.getProperty / upsertProperty / removeProperty - Values on blocks:
Editor.upsertBlockProperty / removeBlockProperty / getBlockProperties
2) Important types (from LSPlugin.ts)
PropertySchema
export type PropertySchema = {
type: 'default' | 'number' | 'node' | 'date' | 'checkbox' | 'url' | string,
cardinality: 'many' | 'one',
hide: boolean
public: boolean
}
Practical meaning:
type- Controls editor/UI behavior and (in DB graphs) how values are interpreted.
- Common:
default,number,date,checkbox,url,node.
cardinality'one': a single value'many': multiple values (typically passed as an array)
hide- Hide in UI property panels.
public- Expose property in UI (and typically configure discoverability).
Entities you’ll see
BlockEntity: blocks, but also used for some “special blocks” such as property entities.PageEntity: pages, tags (classes), property pages (type: 'property'), etc.BlockIdentity: a block uuid string OR{ uuid }.
3) DB-only vs graph-agnostic checks
Before doing DB-only work, check graph type:
const isDbGraph = await logseq.App.checkCurrentIsDbGraph()
if (!isDbGraph) {
await logseq.UI.showMsg('This feature requires a DB graph.', 'warning')
return
}
// Also check app-level capability:
const { supportDb } = await logseq.App.getInfo()
4) Property schema APIs (DB only)
Get a property definition
const propEntity = await logseq.Editor.getProperty('zotero_key')
// -> BlockEntity | null
Create or update a property definition (idempotent)
Use upsertProperty to ensure the property exists and has the schema you expect.
await logseq.Editor.upsertProperty(
'zotero_key',
{
type: 'default',
cardinality: 'one',
hide: false,
public: true,
},
{ name: 'Zotero Key' }
)
Notes:
keyis your stable identifier (recommendsnake_case).opts.namecan be used as a display name for users.
Remove a property definition
await logseq.Editor.removeProperty('zotero_key')
5) Block/page property value APIs
Set (upsert) a property value on a block
const block = await logseq.Editor.getCurrentBlock()
if (!block) return
await logseq.Editor.upsertBlockProperty(block.uuid, 'zotero_key', 'ABCD1234')
Set multi-value properties (cardinality: 'many')
For many, pass an array. Use { reset: true } when you want to overwrite vs merge.
await logseq.Editor.upsertBlockProperty(
block.uuid,
'authors',
['Ada Lovelace', 'Alan Turing'],
{ reset: true }
)
Remove a value
await logseq.Editor.removeBlockProperty(block.uuid, 'zotero_key')
Read properties
const props = await logseq.Editor.getBlockProperties(block.uuid)
// -> Record<string, any> | null
const pageProps = await logseq.Editor.getPageProperties('My Page')
Read a single property (returns a BlockEntity | null)
getBlockProperty is useful when you want the DB entity wrapper for a key.
const v = await logseq.Editor.getBlockProperty(block.uuid, 'zotero_key')
6) Tags as “classes” + attaching properties (DB modeling)
In DB graphs, tags can behave like classes.
Relevant APIs:
Editor.createTag(tagName, opts)Editor.addTagProperty(tagId, propertyIdOrName)/removeTagPropertyEditor.addTagExtends(tagId, parentTagIdOrName)/removeTagExtendsEditor.addBlockTag(blockId, tagId)/removeBlockTag
Create a tag with tagProperties
This is the most developer-friendly way to ship a “schema bundle”:
const tag = await logseq.Editor.createTag('ZoteroItem', {
tagProperties: [
{
name: 'zotero_key',
schema: { type: 'default', cardinality: 'one', public: true, hide: false },
},
{
name: 'authors',
schema: { type: 'node', cardinality: 'many', public: true, hide: false },
},
{
name: 'published_at',
schema: { type: 'date', cardinality: 'one', public: true, hide: false },
},
],
})
if (tag) {
const block = await logseq.Editor.getCurrentBlock()
if (block) await logseq.Editor.addBlockTag(block.uuid, tag.uuid)
}
Why this pattern is popular:
- Users see a consistent “record type” (
ZoteroItem) with fields. - Plugins can be idempotent: create if missing, otherwise just reuse.
Add properties to an existing tag later
const tag = await logseq.Editor.getTag('ZoteroItem')
if (tag) {
await logseq.Editor.addTagProperty(tag.uuid, 'zotero_key')
await logseq.Editor.addTagProperty(tag.uuid, 'authors')
}
Tag inheritance
await logseq.Editor.addTagExtends('ZoteroItem', 'Reference')
7) Recommended conventions for plugin authors
Prefer DB APIs over text editing in DB graphs
- Don’t generate
key:: valuestrings as your “source of truth” in DB mode. - Use
upsertProperty+upsertBlockProperty.
Keep keys stable; use display names for UX
- Use safe keys (
snake_case, ASCII) forkey. - Use
upsertProperty(key, schema, { name })to show a nice label.
Be explicit about cardinality
- If you intend a list, set
cardinality: 'many'and always write arrays. - Use
{ reset: true }when you want to overwrite.
Guard DB-only APIs
- Always check
App.checkCurrentIsDbGraph().
8) A tiny helper wrapper (optional)
If you’re building a small internal “DB properties SDK” for your plugin, a minimal shape looks like:
export async function ensureDbGraph() {
if (!(await logseq.App.checkCurrentIsDbGraph())) {
throw new Error('DB graph required')
}
}
export async function ensureProperty(
key: string,
schema: Partial<import('../src/LSPlugin').PropertySchema>,
name?: string
) {
return logseq.Editor.upsertProperty(key, schema, name ? { name } : undefined)
}
export async function setProp(
block: string,
key: string,
value: any,
reset = false
) {
return logseq.Editor.upsertBlockProperty(block, key, value, reset ? { reset: true } : undefined)
}
Appendix: API quick reference (from LSPlugin.ts)
Property schema (DB only)
Editor.getProperty(key)Editor.upsertProperty(key, schema?, opts?)Editor.removeProperty(key)
Block/page values
Editor.upsertBlockProperty(block, key, value, { reset? })Editor.removeBlockProperty(block, key)Editor.getBlockProperty(block, key)Editor.getBlockProperties(block)Editor.getPageProperties(page)
Tags / Classes
Editor.createTag(tagName, { tagProperties? })Editor.getTag(nameOrIdent)/Editor.getAllTags()Editor.addTagProperty(tagId, propertyIdOrName)/removeTagPropertyEditor.addTagExtends(tagId, parentTagIdOrName)/removeTagExtendsEditor.addBlockTag(blockId, tagId)/removeBlockTag