Create notes, inject Layer 1 content, and commit to vault. Canonical pipeline shared by triage-worker agent and quick-create skill. Not user-invocable — loaded as reference knowledge.
From para-obsidiannpx claudepluginhub nathanvale/side-quest-marketplace-old --plugin para-obsidianThis skill uses the workspace's default tool permissions.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Canonical pipeline for creating notes from enriched content. This skill is the single source of truth for:
para_create pattern for all templatespara_replace_section with formatting rulespara_commit after creationCallers (triage-worker, quick-create) handle enrichment, classification, user confirmation, and persistence separately. This skill only covers the "create → inject → commit" pipeline.
Enriched content + classification (from caller)
↓
1. Discover template metadata (para_template_fields)
↓
2. Create note (para_create)
- Resources: pass Layer 1 via `content` parameter → single call creates + injects + commits
- Meetings: pass structured body via `content` parameter → single call creates + injects + commits
- Others: frontmatter-only → auto-commits
Key optimization: Resources and meetings both use the content parameter on para_create to inject body content in a single CLI invocation. The CLI internally calls createFromTemplate → replaceSections → autoCommitChanges — no separate para_commit or para_replace_section needed.
CRITICAL: Never pass null values in args to para_create. Omit the key entirely.
// WRONG — creates "[[null]]" in frontmatter
args: { projects: null, author: null }
// CORRECT — omit keys with no value
args: { } // projects and author omitted
Apply this rule to ALL optional fields: projects, project, author, transcription, source.
Before creating notes, content must be enriched. See the canonical routing table and tool selection:
@plugins/para-obsidian/skills/triage/references/enrichment-strategies.md
Quick summary: YouTube → get_transcript, Articles/GitHub → firecrawl_scrape, X/Twitter → X-API MCP (x_get_tweet), Voice/Attachment → para_read.
Use the para-classifier skill for determining template, resource type, source format, area, and project:
All templates use the same flow. The creation layer auto-resolves destinations and filters invalid args.
If template fields are provided in your prompt context (e.g., during triage), skip this step — use the pre-loaded values directly. The coordinator pre-fetches these once and passes them to all subagents.
Otherwise, call:
para_template_fields({
template: proposed_template,
response_format: "json"
})
Response includes:
creation_meta.dest — default destination directorycreation_meta.titlePrefix — emoji prefix (auto-applied by para_create; do NOT add to title manually)creation_meta.sections — body section headings (if any)creation_meta.contentTargets — section headings for content injection (e.g., ["Layer 1: Captured Notes"])validArgs — list of accepted field names for this templatefrontmatter_hints — enum values, types, and constraintsFrom your classification output, build args using only fields listed in validArgs. Omit any field with a null/empty value.
// Generic pattern — works for ALL templates
para_create({
template: proposed_template,
title: proposed_title,
args: {
// Only include fields from validArgs
// Omit null values entirely
...classifiedFields
},
response_format: "json"
})
Destination auto-resolved: para_create uses the template's configured default destination. No need to pass dest unless overriding.
Invalid args filtered: Any arg not in the template's frontmatter rules is silently filtered with a log warning. This prevents typos or stale field names from polluting frontmatter.
| Template | How | Post-create steps |
|---|---|---|
resource | Pass Layer 1 via content parameter | None — para_create handles inject + commit |
meeting | Pass structured body via content parameter | None — para_create handles inject + commit |
invoice | Frontmatter-only | None — auto-committed |
booking | Frontmatter-only | None — auto-committed |
| All others | Frontmatter-only | None — auto-committed |
Resources use the content parameter just like meetings. Discover the target section heading from creation_meta.contentTargets[0] via para_template_fields, then pass it as a key in the content parameter. The CLI internally calls replaceSections() then autoCommitChanges() — no separate para_commit or para_replace_section calls needed.
Resources pass Layer 1 content via the content parameter. Discover the content target heading from creation_meta.contentTargets[0] via para_template_fields (typically "Layer 1: Captured Notes"):
para_create({
template: "resource",
title: proposed_title,
args: { ...fields from validArgs },
content: {
"<discovered-content-target>": formattedLayerOneContent
},
response_format: "json"
})
Why this works: create.ts calls createFromTemplate → replaceSections → autoCommitChanges internally when content is provided. This replaces the old 4-step flow (create → commit → replace_section → commit) with a single tool call, saving ~3-6s per item.
Meetings also use the content parameter to pass structured body sections. Discover section headings from creation_meta.sections via para_template_fields:
para_create({
template: "meeting",
title: proposed_title,
args: { ...fields from validArgs },
content: {
"<discovered-attendees-section>": attendees.map(a => `- ${a}`).join('\n'),
"<discovered-notes-section>": meeting_notes.map(n => `- ${n}`).join('\n'),
"<discovered-decisions-section>": decisions.map(d => `- ${d}`).join('\n'),
"<discovered-action-items-section>": action_items.map(i => `- ${i.task}`).join('\n'),
"<discovered-follow-up-section>": follow_up.map(f => `- ${f}`).join('\n')
},
response_format: "json"
})
Field names vary across templates. Always use validArgs from para_template_fields to discover the correct field names for each template. Key differences to be aware of:
areas, projects) with YAML array supportarea, project) for single wikilinksmeeting_type, resource_type)Call para_template_fields per template to get the authoritative field list.
Resource areas and projects are YAML arrays. Obsidian requires proper list format:
# Correct YAML list (Obsidian renders as List property)
areas:
- "[[🤖 AI Practice]]"
- "[[🌱 Home Server]]"
# WRONG — single string, not a list
areas: "[[🤖 AI Practice]], [[🌱 Home Server]]"
How to pass to para_create: The args parameter is Record<string, string>, so encode arrays as JSON strings. para_create parses these automatically via tryParseJsonArray().
// Single area (unchanged — backward compatible)
args: { areas: "[[🌱 AI Practice]]" }
// Multiple areas — JSON array string
args: { areas: '["[[🤖 AI Practice]]", "[[🌱 Home Server]]"]' }
Post-creation via para_fm_set: Same pattern — pass JSON array strings:
para_fm_set({ file, set: { areas: '["[[Area 1]]", "[[Area 2]]"]' } })
Meeting/Invoice/Booking use singular area/project fields (single wikilink, not arrays). Multi-value does not apply to these templates.
During triage: Post-creation verification is handled by the coordinator (Phase 2.5), not the worker. Workers set verification_status: "pending_coordinator" and skip this section. The steps below apply to standalone callers (quick-create) only.
After creating a note, verify that critical frontmatter fields match your intended values. This catches two failure modes:
para_create received wrong/empty args (self-repairable)| Template | Fields to verify | Action |
|---|---|---|
resource | summary, areas, source_format, resource_type | ALWAYS verify |
meeting | summary, area, meeting_type | ALWAYS verify |
invoice/booking | — | SKIP → verification_status: "skipped" |
para_fm_get({ file: created_path, response_format: "json" })
For each critical field, run both checks:
A. MISMATCH CHECK: You intended a non-empty value but the file has empty string "", empty array [], or "[[null]]".
→ Add to repair_set (will fix via para_fm_set)
B. EMPTY CHECK: Both your intended value AND the file value are empty/null.
→ Add to verification_issues (flag for user review)
→ This means you couldn't classify it — the user needs to decide
para_fm_set({ file: created_path, set: repair_set, response_format: "json" })
One call, all fixes batched. For array fields (e.g., areas), pass JSON array strings: '["[[Area 1]]", "[[Area 2]]"]'.
| Outcome | verification_status | verification_issues |
|---|---|---|
| All fields match | "verified" | [] |
| Mismatches found AND repaired | "repaired" | [] |
| Repair failed | "failed" | ["repair failed: {detail}"] |
| Empty fields (can't classify) | "needs_review" | ["missing: areas", ...] |
| Invoice/booking (skipped) | "skipped" | [] |
Include both fields in your proposal output (TaskUpdate metadata and PROPOSAL_JSON).
Analysis determined: summary = "How To Make Your Agent Learn And Ship While You Sleep"
But para_create args omitted summary (bug: value lost during arg construction).
para_fm_get returns: { summary: "" }para_fm_set({ file, set: { summary: "How To Make Your Agent Learn And Ship While You Sleep" } })verification_status: "repaired", verification_issues: ["repaired: summary"]When NOT using the content parameter: Call para_commit after para_create if you're creating frontmatter-only notes outside the triage pipeline (e.g., invoices, bookings).
para_commit({
message: "Add [template]: [title]",
response_format: "json"
})
When using content parameter: No separate commit needed — para_create auto-commits after injection.
Layer 1 content injected via the content parameter (resources) should follow these formatting rules.
Use #### headings or deeper (never #, ##, or ### — those are reserved for note structure).
| Source | Strategy | Target Length |
|---|---|---|
| Article | First 3 paragraphs + key H2/H3 headings with topic sentences + conclusion | 2-3k tokens |
| YouTube | ~10% sampled transcript segments with timestamps | 2-3k tokens |
| Thread | Full thread content in order | Keep all |
| Voice memo | Full transcription if <2k tokens, else key segments | Variable |
| Attachment | Key passages with page references | 2-3k tokens |
For non-triage callers that need separate injection (e.g., quick-create):
para_replace_section({
file: "<created-file-path>",
heading: "<discovered-content-target>",
content: "<formatted-content>",
response_format: "json"
})
Then commit:
para_commit({
message: "Add Layer 1: [title]",
response_format: "json"
})
| Step | Failure | Action |
|---|---|---|
para_create (with content) | Fails | Stop pipeline. Set created: null, layer1_injected: null. Report error. |
para_create (without content) | Fails | Stop pipeline. Set created: null. Report error. |
para_commit (standalone callers) | Fails | Note exists but isn't committed. Continue if possible. |
para_replace_section (standalone callers) | Fails | Keep the note. Set layer1_injected: false. Resource still usable. |
Soft failure philosophy: Note creation is primary. Layer 1 injection and commit are enhancements. Don't block note creation if downstream steps fail.
See @plugins/para-obsidian/skills/triage/references/proposal-schema.md for the canonical schema.
Key field names: area (single wikilink or array of wikilinks), project (single wikilink, array, or null), resourceType (camelCase, NOT resource_type).