Skills move out of packages/core/src/application/assistant/skills/*/skill.ts
(TS string constants) into apps/skills/<id>/SKILL.md (Agent Skills spec format
— YAML frontmatter + markdown body). One directory, one loader, one place to
look at every skill the agent can load.
Key change vs the old dev system: a `{{include:<skill-id>}}` directive lets one
skill transclude another. This removes the parallel TS constant for the
knowledge-note style guide — it now lives at apps/skills/knowledge-note-style/
(hidden from catalog) and is pulled into doc-collab + the live-note and
background-task agents via the resolver instead of via a TS import.
Infrastructure:
- packages/core/src/skills/ — types, skill-md-parser, FS-backed official repo,
SkillResolver with recursive {{include:<id>}} expansion + cycle detection
- packages/shared/src/skill.ts — SkillFrontmatter, SkillCatalogEntry,
ResolvedSkill schemas
- DI: officialSkillsRepo + skillResolver registered; registerSkillsDir helper
wires the path before any consumer resolves
- IPC: skills:list / skills:get (read-only) for the Settings UI
- Main: resolveSkillsDir picks Resources/skills (packaged) or repo apps/skills
(dev). forge.config.cjs ships apps/skills/ as extraResource.
Consumer refactor:
- buildCopilotInstructions: catalog markdown built from resolver.getCatalog()
- builtin-tools: loadSkill uses resolver, new listSkills tool
- background-tasks/agent + live-note/agent: now async builders that load
the knowledge-note-style skill content via resolver
- runtime.loadAgent: awaits the now-async builders
- Deleted: assistant/skills/ directory, knowledge-note-style.ts
UI:
- New SkillsSettings component (read-only list + detail view) wired into
Settings dialog as the "Skills" tab.
9 KiB
| name | description | metadata | ||
|---|---|---|---|---|
| background-task | Set up a recurring background task — persistent instructions the agent fires on a schedule and/or on matching events (Gmail, Calendar). Either maintains an `index.md` digest (OUTPUT mode) or performs a recurring side-effect like drafting a reply / posting to Slack / calling an API (ACTION mode). Flagship surface for anything recurring. |
|
Background Tasks Skill
A background task is a persistent agent the user configures once and the framework keeps firing — on a schedule, inside time-of-day windows, and/or in response to matching incoming events (Gmail threads, calendar changes). Each task lives at bg-tasks/<slug>/ and owns two artifacts:
task.yaml— the spec (the user's instructions, triggers, runtime state). You and the user both treat this as the source of truth.index.md— the agent-owned body. The runtime never writes here; the bg-task agent does, each run.
A task is one of two shapes — the agent decides per run from the verbs in instructions:
| Mode | Trigger verbs | Behavior |
|---|---|---|
| OUTPUT | "maintain / show / summarize / track / digest" | Rewrite index.md to reflect the current state. |
| ACTION | "send / draft / post / notify / file / reply / call" | Perform the action, then append a one-line journal entry under ## Journal in index.md. |
Mixed instructions ("summarize and email it") trigger both.
Tools you'll use (and ones you WON'T)
You have three dedicated builtin tools for this skill:
create-background-task— materializes a new task on disk. Use this. Do not writetask.yamlyourself withworkspace-edit, and do not search the codebase for IPC channels likebg-task:create— they're renderer-side and not callable from here.patch-background-task— updates an existing task (instructions / triggers / active / model). Use this for the extend-don't-fork case.run-background-task-agent— manually fires a task to run now. Always call this immediately aftercreate-background-taskso the user sees content.
To inspect what tasks already exist, use workspace-glob on bg-tasks/*/task.yaml and workspace-readFile on candidates. The user's bg-tasks folder is workspace-relative.
Mode: act-first
Bg-task creation is action-first. Don't ask "should I?" — read the request, pick a name, call create-background-task, then call run-background-task-agent with the returned slug. Confirm in one line past-tense at the end. Tell the user the surface name: "Manage it from Background tasks in the sidebar."
The only exception: if a related bg-task already exists, extend its instructions via patch-background-task rather than creating a duplicate (see "Extend, don't fork").
When you're loaded
The host's trigger paragraph loads this skill on:
- Cadence: "every morning", "daily", "hourly", "each Monday"
- Watch/monitor: "watch / monitor / keep an eye on / track / follow X"
- Recurring artifact: "morning briefing", "weekly review", "Acme deal dashboard"
- Event-conditional: "whenever a relevant email comes in, …"
- Action verbs: "draft / reply / call / post / notify / file / brief me on"
- Decay questions: "what's the weather", "top HN stories", "latest on X" — answer the one-off, then offer
If the user explicitly says "live note" / "live-note", the host loads the live-note skill instead — don't try to handle that case here.
Workflow
-
Check for existing tasks. Before creating, glob
bg-tasks/*/task.yamland read any candidates whose intent might overlap with the user's ask. If a related task exists, jump to "Extend, don't fork" below. -
Pick a name. Use a short, friendly title in title-case: "Morning weather", "Q3 deal digest", "HN top stories". The framework slugifies it (lowercase, dashes) for the folder — you don't manage the slug.
-
Write the instructions. Capture the user's intent in their own words, with concrete verbs. Bake any specifics (which source, which audience, output shape) into the instructions — the agent re-reads them on every run.
- Good: "Summarize my unread emails since yesterday 6pm into a one-paragraph digest plus a bulleted list of action items. Skip newsletters and automated notifications."
- Bad: "Daily email summary." (vague — agent will improvise unhelpfully)
-
Pick triggers. All three are independently optional; mix freely.
cronExpr— exact times."0 7 * * *"= 7am daily.windows— time-of-day bands. Each fires once per day inside the band, anywhere — forgiving when the app was offline.eventMatchCriteria— a natural-language description of which incoming events should wake the task (e.g. "Emails about Q3 OKRs from the leadership team"). Pass-1 routing matches; the agent does Pass-2 before acting.
No triggers at all = manual-only. The user clicks Run.
-
Call
create-background-task. Required:name,instructions. Optional:triggers,model,provider(leave model/provider unset unless the user explicitly asked). The tool returns a slug. -
Call
run-background-task-agentwith the slug. The agent runs once and populatesindex.md. -
Confirm. One line. Name the task. Point at the sidebar. Done.
Extend, don't fork
When the user's new ask overlaps with an existing task — e.g. they say "also include X" or the ask is a refinement of an existing task's intent — call patch-background-task instead of creating a duplicate.
Signals that you should extend:
- The user says "also …" / "and on top of that …" / "while you're at it …"
- The new ask is a refinement of an existing task's intent (different threshold, additional source, slightly different output)
When extending, pass the full rewritten instructions — don't try to surgical-edit a single sentence. The agent rereads instructions every run, so a clean rewrite is fine. After patch-background-task returns, call run-background-task-agent on the same slug so the user sees the updated output.
Worked examples
OUTPUT — morning briefing
User: "Every morning at 7, give me a one-paragraph summary of overnight news in AI agents."
create-background-taskwith:name: "AI agent overnight news"instructions: "Search the web and Hacker News for news about AI agents (autonomous LLM agents, agentic frameworks, agent benchmarks) published in the last 24 hours. Summarize the top developments in one paragraph (3-5 sentences) followed by a 3-5 item bulleted list of the most significant items with a single-sentence note each. Replace the body of index.md."triggers: {cronExpr: "0 7 * * *" }
run-background-task-agentslug=ai-agent-overnight-news.- "Done — created the AI agent overnight news task. It'll run every morning at 7 and you can find it in Background tasks in the sidebar."
ACTION — email auto-reply
User: "Whenever I get an email about Q3 planning, draft a reply asking when they're free this week."
create-background-taskwith:name: "Q3 email auto-reply drafts"instructions: "When an event arrives describing an email thread about Q3 planning, use the Gmail draft-create tool to draft a reply to the latest message asking the sender when they're free for a 30-minute call this week. Do not send the draft — leave it in Drafts for me to review. After drafting, append a journal entry to index.md noting the thread subject and the draft id."triggers: {eventMatchCriteria: "Emails about Q3 planning (roadmap, OKRs, headcount, exec priorities)" }
run-background-task-agentslug=q3-email-auto-reply-drafts.- "Done — created the Q3 email auto-reply drafts task. It'll fire on relevant Gmail threads. Manage it from Background tasks in the sidebar."
ACTION + journal — Slack watcher
User: "Every weekday morning at 9, post a summary of unresolved high-priority issues to #engineering on Slack."
create-background-taskwith:name: "Daily eng triage"instructions: "Each run, query for unresolved issues labeled priority:high or above. Summarize counts by owner and the three oldest items. Send the summary to #engineering via the Slack tool. After sending, append a journal entry to index.md with the timestamp and the message id."triggers: {cronExpr: "0 9 * * 1-5" }
run-background-task-agentslug=daily-eng-triage.
Canonical Schema
${schemaYaml}
Notes:
activedefaults to true. Patch{ active: false }to pause without deleting.createdAtandlastRunare runtime-managed — never write them yourself.- The
triggersblock reuses Live Notes'Triggersschema verbatim. Cron grace and 5-minute backoff semantics are identical.
Exceptions
The Background tasks sidebar view has a "New task" button that opens a form-driven flow. If the user is editing fields there or asking about a specific task from that view, you are not the right surface — the form is. Point at it ("You can also do this from the New task button in the Background tasks view") and step aside.