Create a new inbox document classifier with guided wizard
Creates a complete inbox document classifier with guided wizard for detection patterns, field extraction, and template mapping.
/plugin marketplace add nathanvale/side-quest-marketplace/plugin install claude-code-docs@side-quest-marketplaceclassifier-nameclaude-sonnet-4-5-20250929Generate a complete inbox document classifier following the para-obsidian patterns.
Inbox classifiers detect, extract, and convert documents from the inbox folder into structured notes. This command guides you through creating:
definitions/{id}.ts) - Detection patterns, field extraction, template mappingDEFAULT_CLASSIFIERS arrayYou are a classifier scaffolding specialist. Create well-structured inbox classifiers using established patterns.
The classifier name/id is provided as $1 (or $ARGUMENTS).
Classifier ID must be in kebab-case format:
receipt, medical-statement, travel-booking, tax-returnCheck for conflicts:
definitions/{id}.ts doesn't already existDEFAULT_CLASSIFIERS in definitions/index.ts for duplicatesUse AskUserQuestion to collect:
Question 1: "What type of documents should this classifier detect?"
Example answers: "Tax returns from the ATO", "Restaurant receipts", "Medical appointment confirmations"
Question 2: "What priority should this classifier have? (0-100, higher = checked first)"
Provide context:
110+: Very specific (e.g., medical-statement for specific provider)100: Standard documents (e.g., invoice)90: Common types (e.g., booking)80-85: Less common (e.g., research) Default to85if unsure.
Question 3: "Which PARA area does this typically belong to?"
Provide options: Finance, Health, Work, Home, Travel, Personal, or "varies/none"
Use AskUserQuestion to collect pattern information:
Question 4: "What words/patterns typically appear in the FILENAME?"
Example: "tax, return, ato, myGov" (comma-separated) These become
filenamePatternswith weights (first = 1.0, subsequent = 0.8, 0.7, etc.)
Question 5: "What words/phrases typically appear in the CONTENT?"
Example: "Tax file number, TFN, assessment notice, Notice of Assessment" (comma-separated) These become
contentMarkerswith weights
Use AskUserQuestion to define fields:
Question 6: "What fields should be extracted? List them with types."
Provide format guidance:
Format: fieldName:type:requirement (requirement optional, defaults to 'optional')
Types: string, date, currency, number
Requirements: required, optional, conditional
Examples:
- title:string:required
- assessmentDate:date:required
- taxableIncome:currency:optional
- financialYear:string:required
- refundAmount:currency:conditional
Question 7: "For each required/key field, provide a brief description for the LLM."
Use AskUserQuestion for template details:
Question 8: "What should the template filename be (without .md)?"
Default to the classifier ID. Example:
tax-return
After getting the template name, check if the template already exists in ${PARA_VAULT}/Templates/:
If template exists:
Ask user: "Template {name}.md already exists. What would you like to do?"
Options:
tax-return-v2.md)If template doesn't exist:
Ask user: "Would you like to create a template for this classifier?"
Options:
Question 9: "What are the Templater prompt labels for each field?"
Format:
fieldName: Prompt LabelExample:title: Document title assessmentDate: Assessment date (YYYY-MM-DD) taxableIncome: Taxable income amount
Generates a functional template with:
Example basic template:
---
type: tax-return
template_version: 1
created: <% tp.date.now("YYYY-MM-DD") %>
title: "<% tp.system.prompt("Document title") %>"
assessmentDate: "<% tp.system.prompt("Assessment date (YYYY-MM-DD)") %>"
financialYear: "<% tp.system.prompt("Financial year") %>"
taxableIncome: "<% tp.system.prompt("Taxable income amount") %>"
area: "[[Finance]]"
---
# <% tp.system.prompt("Document title") %>
## Details
**Assessment Date**: <% tp.system.prompt("Assessment date (YYYY-MM-DD)") %>
**Financial Year**: <% tp.system.prompt("Financial year") %>
**Taxable Income**: <% tp.system.prompt("Taxable income amount") %>
## Notes
<% tp.system.prompt("Additional notes (optional)") %>
---
*Processed from inbox: <% tp.date.now("YYYY-MM-DD HH:mm") %>*
Invokes the template-assistant skill to create enhanced templates with:
The skill receives:
Ask about confidence thresholds (optional - use defaults if skipped):
Question 10: "Do you want custom scoring thresholds? (yes/no)"
If yes:
Generate plugins/para-obsidian/src/inbox/classify/classifiers/definitions/{id}.ts:
/**
* {DisplayName} Classifier
*
* {Description of what this classifier detects}
*
* @module classifiers/definitions/{id}
*/
import type { InboxConverter } from "../types";
/**
* {DisplayName} classifier for {description}
*/
export const {camelCaseId}Classifier: InboxConverter = {
schemaVersion: 1,
id: "{id}",
displayName: "{DisplayName}",
enabled: true,
priority: {priority},
heuristics: {
filenamePatterns: [
// Generated from user input, with decreasing weights
{ pattern: "{pattern1}", weight: 1.0 },
{ pattern: "{pattern2}", weight: 0.9 },
// ...
],
contentMarkers: [
// Generated from user input
{ pattern: "{marker1}", weight: 1.0 },
{ pattern: "{marker2}", weight: 0.9 },
// ...
],
threshold: 0.3,
},
fields: [
// Always include title as first field
{
name: "title",
type: "string",
description: "{DocumentType} title/description",
requirement: "required",
},
// User-defined fields...
],
extraction: {
promptHint: "{LLM prompt hint for this document type}",
keyFields: [{keyFields}],
},
template: {
name: "{templateName}",
fieldMappings: {
// Map field names to Templater prompt labels
},
},
scoring: {
heuristicWeight: {heuristicWeight},
llmWeight: {llmWeight},
highThreshold: {highThreshold},
mediumThreshold: {mediumThreshold},
},
};
Update plugins/para-obsidian/src/inbox/classify/classifiers/definitions/index.ts:
Add import:
import { {camelCaseId}Classifier } from "./{id}";
Add export:
export { {camelCaseId}Classifier } from "./{id}";
Add to DEFAULT_CLASSIFIERS array (maintain priority order):
export const DEFAULT_CLASSIFIERS: readonly InboxConverter[] = [
// Existing classifiers...
{camelCaseId}Classifier, // Priority {priority}
] as const;
Type check:
cd plugins/para-obsidian && bun typecheck
Run classifier tests:
bun test classifiers
Verify registration:
bun -e "
const { DEFAULT_CLASSIFIERS } = require('./src/inbox/classify/classifiers/definitions');
const found = DEFAULT_CLASSIFIERS.find(c => c.id === '{id}');
console.log(found ? '✓ Classifier registered' : '✗ Not found');
console.log('Total classifiers:', DEFAULT_CLASSIFIERS.length);
"
After generation, inform the user they need to create a matching template:
Create Templates/{templateName}.md in your Obsidian vault:
---
type: {noteType}
template_version: 1
created: <% tp.date.now("YYYY-MM-DD") %>
{frontmatterFields}
---
# <% tp.user.prompt("{titlePrompt}") %>
## Details
{templateBodySuggestion}
---
*Created from inbox: <% tp.file.title %>*
Provide specific field mappings based on their classifier definition.
User invokes: /para-obsidian:create-classifier tax-return
Step 1 - Basic Info:
Q1: What type of documents should this classifier detect?
A: Australian Tax Office (ATO) tax returns and notices of assessment
Q2: What priority should this classifier have? (0-100)
A: 95
Q3: Which PARA area does this typically belong to?
A: Finance
Step 2 - Heuristic Patterns:
Q4: What words/patterns appear in the FILENAME?
A: tax, return, ato, assessment, noa, mygov
Q5: What words/phrases appear in the CONTENT?
A: Notice of Assessment, Tax file number, TFN, taxable income, tax offset
Step 3 - Field Extraction:
Q6: What fields should be extracted?
A:
title:string:required
assessmentDate:date:required
financialYear:string:required
taxableIncome:currency:required
taxPayable:currency:optional
taxOffset:currency:optional
refundAmount:currency:conditional
debtAmount:currency:conditional
Q7: Provide descriptions for required fields:
A:
- title: Document title or reference number
- assessmentDate: Date the assessment was issued
- financialYear: Tax year (e.g., "2023-24")
- taxableIncome: Total taxable income for the year
Step 4 - Template Configuration:
Q8: Template filename?
A: tax-return
[System checks: Templates/tax-return.md not found]
Q9: Create template? (basic/rich/skip)
A: basic
Q10: Templater prompt labels for each field?
A:
title: Document title
assessmentDate: Assessment date (YYYY-MM-DD)
financialYear: Financial year
taxableIncome: Taxable income amount
Step 5 - Scoring:
Q11: Custom scoring thresholds?
A: no (use defaults)
definitions/tax-return.ts/**
* Tax Return Classifier
*
* Australian Tax Office (ATO) tax returns and notices of assessment
*
* @module classifiers/definitions/tax-return
*/
import type { InboxConverter } from "../types";
export const taxReturnClassifier: InboxConverter = {
schemaVersion: 1,
id: "tax-return",
displayName: "Tax Return",
enabled: true,
priority: 95,
heuristics: {
filenamePatterns: [
{ pattern: "tax", weight: 1.0 },
{ pattern: "return", weight: 0.9 },
{ pattern: "ato", weight: 0.8 },
{ pattern: "assessment", weight: 0.7 },
{ pattern: "noa", weight: 0.6 },
{ pattern: "mygov", weight: 0.5 },
],
contentMarkers: [
{ pattern: "Notice of Assessment", weight: 1.0 },
{ pattern: "Tax file number", weight: 0.9 },
{ pattern: "TFN", weight: 0.8 },
{ pattern: "taxable income", weight: 0.7 },
{ pattern: "tax offset", weight: 0.6 },
],
threshold: 0.3,
},
fields: [
{
name: "title",
type: "string",
description: "Document title or reference number",
requirement: "required",
},
{
name: "assessmentDate",
type: "date",
description: "Date the assessment was issued",
requirement: "required",
},
{
name: "financialYear",
type: "string",
description: "Tax year (e.g., '2023-24')",
requirement: "required",
},
{
name: "taxableIncome",
type: "currency",
description: "Total taxable income for the year",
requirement: "required",
},
{
name: "taxPayable",
type: "currency",
description: "Total tax payable",
requirement: "optional",
},
{
name: "taxOffset",
type: "currency",
description: "Tax offset amount",
requirement: "optional",
},
{
name: "refundAmount",
type: "currency",
description: "Refund amount if applicable",
requirement: "conditional",
},
{
name: "debtAmount",
type: "currency",
description: "Debt amount if applicable",
requirement: "conditional",
},
],
extraction: {
promptHint: "Extract key details from ATO tax return or notice of assessment",
keyFields: ["title", "assessmentDate", "financialYear", "taxableIncome"],
},
template: {
name: "tax-return",
fieldMappings: {
title: "Document title",
assessmentDate: "Assessment date (YYYY-MM-DD)",
financialYear: "Financial year",
taxableIncome: "Taxable income amount",
taxPayable: "Tax payable",
taxOffset: "Tax offset",
refundAmount: "Refund amount",
debtAmount: "Debt amount",
},
},
scoring: {
heuristicWeight: 0.3,
llmWeight: 0.7,
highThreshold: 0.85,
mediumThreshold: 0.6,
},
};
definitions/index.ts// Added import
import { taxReturnClassifier } from "./tax-return";
// Added export
export { taxReturnClassifier } from "./tax-return";
// Added to DEFAULT_CLASSIFIERS array (priority order maintained)
export const DEFAULT_CLASSIFIERS: readonly InboxConverter[] = [
medicalStatementClassifier, // Priority 110
taxReturnClassifier, // Priority 95 <-- NEW
invoiceClassifier, // Priority 90
bookingClassifier, // Priority 85
// ... other classifiers
] as const;
Templates/tax-return.md---
type: tax-return
template_version: 1
created: <% tp.date.now("YYYY-MM-DD") %>
title: "<% tp.system.prompt("Document title") %>"
assessmentDate: "<% tp.system.prompt("Assessment date (YYYY-MM-DD)") %>"
financialYear: "<% tp.system.prompt("Financial year") %>"
taxableIncome: "<% tp.system.prompt("Taxable income amount") %>"
taxPayable: "<% tp.system.prompt("Tax payable") %>"
taxOffset: "<% tp.system.prompt("Tax offset") %>"
refundAmount: "<% tp.system.prompt("Refund amount") %>"
debtAmount: "<% tp.system.prompt("Debt amount") %>"
area: "[[Finance]]"
---
# <% tp.system.prompt("Document title") %>
## Details
**Assessment Date**: <% tp.system.prompt("Assessment date (YYYY-MM-DD)") %>
**Financial Year**: <% tp.system.prompt("Financial year") %>
**Taxable Income**: <% tp.system.prompt("Taxable income amount") %>
**Tax Payable**: <% tp.system.prompt("Tax payable") %>
**Tax Offset**: <% tp.system.prompt("Tax offset") %>
**Refund Amount**: <% tp.system.prompt("Refund amount") %>
**Debt Amount**: <% tp.system.prompt("Debt amount") %>
## Notes
<% tp.system.prompt("Additional notes (optional)") %>
---
*Processed from inbox: <% tp.date.now("YYYY-MM-DD HH:mm") %>*
After successful completion, you should have:
✅ Classifier registered and loadable
# Verify classifier is in registry
bun -e "const { DEFAULT_CLASSIFIERS } = require('./src/inbox/classify/classifiers/definitions'); \
console.log(DEFAULT_CLASSIFIERS.find(c => c.id === 'tax-return'))"
✅ TypeScript compiles without errors
cd plugins/para-obsidian && bun typecheck
✅ Template created (if chosen)
ls ${PARA_VAULT}/Templates/tax-return.md
✅ End-to-end inbox processing works
# 1. Place test document in inbox
cp ~/Downloads/ATO-Notice-2024.pdf ${PARA_VAULT}/Inbox/
# 2. Scan inbox
bun run src/cli.ts process-inbox scan
# 3. Verify classifier detected it
# Should show tax-return classifier with high confidence
# 4. Execute suggestion
bun run src/cli.ts process-inbox execute
# 5. Check note was created
ls ${PARA_VAULT}/Finance/Tax\ Returns/
Scenario: Classifier with ID tax-return already exists in definitions/
Handling:
definitions/{id}.ts exists{id} already exists. Overwrite? (yes/no)"Scenario: Template tax-return.md already exists in vault
Handling:
v2, alt)template.name to match final choiceScenario: User provides unsupported field type boolean:required
Handling:
string, date, currency, numberboolean. Allowed: string, date, currency, number"Scenario: User defines field taxOffset but doesn't provide Templater label
Handling:
taxOffset → "Tax Offset")taxOffset: 'Tax Offset'. Edit if needed."Scenario: Power loss or error while updating definitions/index.ts
Handling:
index.ts.backup before modifyingScenario: Multiple classifiers at same priority (e.g., two at priority 95)
Handling:
Scenario: Generated classifier code has syntax errors
Handling:
bun typecheck after generation/para-obsidian:create-classifier againScenario: Area field contains wikilink [[Finance]]
Handling:
area: "[[Finance]]"After creating a classifier, here's how it integrates with inbox processing:
${PARA_VAULT}/Inbox/ATO-Notice-2024.pdf
bun run src/cli.ts process-inbox scan
What happens:
tax-return)ato (0.8), notice (0.7)If heuristic score > threshold (0.3):
tax-return classifier schema(0.3 × 0.75) + (0.7 × 0.92) = 0.87 (high confidence)Output:
Classifier: tax-return (confidence: 0.87)
Suggested title: "Tax Return - 2023-24 Financial Year"
Extracted fields:
- assessmentDate: 2024-10-15 (LLM)
- financialYear: 2023-24 (LLM)
- taxableIncome: $85,450.00 (LLM)
- refundAmount: $1,234.56 (LLM)
bun run src/cli.ts process-inbox execute
Interactive prompts:
Result:
${PARA_VAULT}/Finance/Tax Returns/Tax Return - 2023-24.mdProcessed/ folderWhen user opens the note in Obsidian:
Invalid classifier ID:
TaxReturn → suggest tax-returnDuplicate ID:
{id} already exists. Overwrite? (yes/no)"Priority out of range:
TypeScript compilation fails:
Template validation fails:
File system errors:
chmoddefinitions/ folderTemplates/ in vaultTransaction pattern:
definitions/index.tsOn any failure:
| Type | Description | Validation | Example |
|---|---|---|---|
string | Text value | Non-empty | "ATO Notice" |
date | ISO date | YYYY-MM-DD | "2024-12-01" |
currency | Numeric amount | Number or string | "1250.00" |
number | Integer/float | Numeric | "42" |
| Level | Usage | Validation |
|---|---|---|
required | Must be extracted | Fails if missing |
optional | Nice to have | Succeeds without |
conditional | Sometimes required | Requires conditionalOn field |
definitions/_template.tsclassifiers/types.tsclassifiers/registry.tsclassifiers/loader.tsdefinitions/README.md