// Essential knowledge for developing Logseq plugins for DB (database) graphs. Use this skill when creating or debugging Logseq plugins that work with DB graphs. Claude Code only.
| name | logseq-db-plugin-dev |
| description | Essential knowledge for developing Logseq plugins for DB (database) graphs. Use this skill when creating or debugging Logseq plugins that work with DB graphs. Claude Code only. |
Target: @logseq/libs v0.2.3 | Logseq 0.11.0+ (DB graphs)
This guide focuses on DB (database) graphs, which differ fundamentally from markdown/file-based graphs. Critical differences are highlighted throughout.
{
"name": "logseq-example-plugin",
"version": "1.0.0",
"main": "dist/index.js",
"logseq": {
"id": "logseq-example-plugin",
"title": "Example Plugin",
"icon": "./icon.svg"
},
"dependencies": {
"@logseq/libs": "^0.2.3"
},
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^5.0.0",
"vite-plugin-logseq": "^1.1.2"
}
}
import '@logseq/libs'
async function main() {
console.log('Plugin loaded')
logseq.Editor.registerSlashCommand('My Command', async () => {
// Command implementation
})
}
logseq.ready(main).catch(console.error)
// Simple page creation
const page = await logseq.Editor.createPage(
'Page Name',
{
property1: 'value1',
property2: 'value2'
}
)
Key Difference: DB graphs support explicit schema definitions and automatic property namespacing.
// Page with explicit schema (v0.2.3+)
const page = await logseq.Editor.createPage(
'Article Title',
{
title: 'My Article',
year: 2025,
verified: true,
url: 'https://example.com'
},
{
schema: {
title: { type: 'string' },
year: { type: 'number' },
verified: { type: 'checkbox' },
url: { type: 'url' }
},
redirect: false,
createFirstBlock: false
} as any // TypeScript defs not yet updated
)
Property Types:
string - Textnumber - Numberscheckbox - Boolean checkboxesurl - URLs (validated)date - Date valuesdatetime - Date with timepage - Page referencesnode - Block referencesImportant:
:plugin.property.{plugin-id}/{property-name}as any for schema parameter (TypeScript definitions lag behind runtime API)Not applicable - tags are just page references.
Key Difference: DB graphs have dedicated class/tag pages, but plugin API cannot set class property schemas.
// Create a class page (v0.2.3+)
const tagPage = await logseq.Editor.createPage(
'research',
{},
{
class: true
} as any
)
IMPORTANT LIMITATION: The schema parameter on createPage() creates page-level schemas, NOT class schemas. Plugins cannot programmatically set the :logseq.property.class/properties field due to security restrictions.
Instead of class schemas, use property entities:
// Step 1: Create property entities with schemas
await logseq.Editor.upsertProperty('year', {
type: 'number'
})
await logseq.Editor.upsertProperty('title', {
type: 'string'
})
await logseq.Editor.upsertProperty('tags', {
type: 'string',
cardinality: 'many'
})
// Step 2: Use properties on pages with explicit schemas
const page = await logseq.Editor.createPage(
'Study on XYZ #research',
{}
)
// Set each property with its schema
await logseq.Editor.upsertBlockProperty(
page.uuid,
'year',
2025,
{ type: 'number' }
)
await logseq.Editor.upsertBlockProperty(
page.uuid,
'title',
'XYZ Study',
{ type: 'string' }
)
Why This Works:
upsertBlockProperty() with schema parameter sets typed values on pagesWhat Cannot Be Done:
// ❌ FAILS - Plugins cannot set class/properties
await logseq.Editor.upsertBlockProperty(
classPage.uuid,
'class/properties',
['year', 'title', 'verified']
)
// Error: "Plugins can only upsert its own properties"
The :logseq.property.class/properties field is a system property that plugins cannot modify. Users must manually add properties to class schemas via the Logseq UI if they want centralized schema management.
// Tags can be added as page properties or in content
const page = await logseq.Editor.createPage(
'My Page',
{ tags: ['tag1', 'tag2'] }
)
Key Difference: Tags must be included in the page title. No explicit addTag() API exists.
// ✅ CORRECT - Tags in page title
const page = await logseq.Editor.createPage(
'My Article #research #science',
{ year: 2025 }
)
// ❌ WRONG - No effect in DB graphs
const page = await logseq.Editor.createPage(
'My Article',
{ tags: ['research', 'science'] } // Creates custom property, not actual tags
)
// ❌ WRONG - addTag() method doesn't exist
await logseq.Editor.addTag(page.uuid, 'research') // Error: "Not existed method"
// Simple block insertion
const block = await logseq.Editor.insertBlock(
parentBlock,
'Block content',
{ sibling: true }
)
Key Difference: Blocks in DB graphs have strict UUID requirements and different property handling.
// Single block with properties
const block = await logseq.Editor.appendBlockInPage(
'Page Name',
'Block content',
{
properties: {
customProp: 'value'
}
}
)
// Batch blocks (nested structure)
const blocks = await logseq.Editor.insertBatchBlock(
parentBlock,
[
{
content: 'Parent block',
children: [
{ content: 'Child 1' },
{ content: 'Child 2' }
]
}
],
{ sibling: false }
)
Important:
properties parameter in insertBatchBlock is not supported in DB graphsinsertBlock with properties option instead for individual blocksDB Graphs Only: Properties can be created as standalone entities with schemas.
// Create a property entity with schema
const property = await logseq.Editor.upsertProperty('customField', {
type: 'string',
cardinality: 'one'
})
// Property is created with namespace
// Result: :plugin.property.my-plugin-id/customfield
// Get property entity
const prop = await logseq.Editor.getProperty('customField')
console.log(prop.id) // Database ID
console.log(prop.type) // 'string'
Available Schema Options:
{
type: 'default' | 'number' | 'date' | 'datetime' | 'checkbox' | 'url' | 'node' | 'json' | 'string',
cardinality: 'one' | 'many', // Default: 'one'
hide?: boolean,
public?: boolean
}
Benefits:
// Set a property on a block/page with schema
await logseq.Editor.upsertBlockProperty(
page.uuid,
'year',
2025,
{ type: 'number' }
)
// Multi-value property
await logseq.Editor.upsertBlockProperty(
page.uuid,
'tags',
['research', 'science'],
{ type: 'string', cardinality: 'many' }
)
When to use upsertBlockProperty() with schema:
When to use upsertProperty() first:
Both MD and DB: Avoid these reserved names - they have special validation:
createdmodifiedupdatedIssue in DB graphs (v0.2.3):
// ❌ PROBLEMATIC - Drops 'title' property
const page = await logseq.Editor.createPage(
'My Page',
{
created: '2025-01-15',
title: 'Will be dropped' // This gets lost
}
)
// ✅ SAFE - Use alternative names
const page = await logseq.Editor.createPage(
'My Page',
{
dateCreated: '2025-01-15',
publicationDate: '2025-01-15',
title: 'Works correctly'
}
)
MD Graphs: Properties stored as-is in frontmatter.
DB Graphs: Plugin properties are automatically namespaced.
// Plugin creates this property:
{ title: 'My Article' }
// Stored in DB as:
:plugin.property.my-plugin-id/title "My Article"
Important: This prevents conflicts but means you can't modify Logseq's built-in properties like :block/tags.
Query markdown files directly or use Datalog on file-based database.
Key Difference: Must use Datalog queries against the database.
// Find pages by property value
const results = await logseq.DB.q(`
[:find (pull ?b [*])
:where
[?b :plugin.property.my-plugin/doi "${doi}"]]
`)
// Check if page exists
const existing = await logseq.DB.q(`
[:find (pull ?b [*])
:where
[?b :block/original-name ?name]
[(= ?name "${pageName}")]]
`)
Important:
:block/original-name for case-sensitive page name lookups[[{page1}], [{page2}]]Can insert HTML directly into markdown.
Key Difference: HTML must be converted to markdown first.
import TurndownService from 'turndown'
const turndownService = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced'
})
// Convert HTML to markdown
const markdown = turndownService.turndown(htmlContent)
const block = await logseq.Editor.appendBlockInPage(
pageName,
markdown
)
Check for existing files.
Key Difference: Query by unique property, not page title.
// ❌ UNRELIABLE - Page titles can change
const existing = await logseq.Editor.getPage(pageName)
// ✅ RELIABLE - Query by unique property (DOI, URL, etc.)
const results = await logseq.DB.q(`
[:find (pull ?b [*])
:where
[?b :plugin.property.my-plugin/doi "${doi}"]]
`)
if (results && results.length > 0) {
console.log('Page already exists')
}
Plugins run in iframes. To test functions from browser console:
// In your plugin code - expose to parent window
if (typeof parent !== 'undefined' && parent.window) {
// @ts-ignore
parent.window.myTestFunction = async function() {
// Test code here
}
}
// In browser console
await window.myTestFunction()
addTag() API:block/tags directlyupsertProperty() and upsertBlockProperty() insteadas any for schema parametersnull or "" to preserve#Tag and #tag are the same| Feature | MD Graphs | DB Graphs |
|---|---|---|
| Page creation | Simple properties | Schema-based with types |
| Property storage | Frontmatter | Namespaced database entities |
| Tags | Property or content | Must be in page title |
| HTML content | Direct insertion | Convert to markdown |
| Duplicate detection | By filename | By unique property query |
| Class pages | Not applicable | Schema inheritance support |
| Block properties | Frontmatter | Database properties |
#tag in title:logseq.property.class/properties is a system property that plugins cannot modify. Users must manually configure class schemas in the UI.schema and class parameters not definedSince plugins cannot set class schemas, use this pattern:
// Create property entities on plugin init
async function initProperties() {
const properties = {
year: { type: 'number' },
title: { type: 'string' },
verified: { type: 'checkbox' }
}
for (const [name, schema] of Object.entries(properties)) {
await logseq.Editor.upsertProperty(name, schema)
}
}
// Use explicit schemas when setting properties on pages
async function createPage(data) {
const page = await logseq.Editor.createPage(
`${data.title} #mytag`,
{}
)
await logseq.Editor.upsertBlockProperty(
page.uuid, 'year', data.year, { type: 'number' }
)
await logseq.Editor.upsertBlockProperty(
page.uuid, 'title', data.title, { type: 'string' }
)
}
This provides typed properties without requiring class schema setup.
upsertProperty() to define reusable properties with schemasupsertBlockProperty() for type safetycreated, modified, updated#tag syntax, not as propertiesparent.window for console testingas any for schema parameters until TypeScript defs updated