Skills til
git clone https://github.com/openclaw/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/openclaw/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/biao29/til" ~/.claude/skills/openclaw-skills-til && rm -rf "$T"
T=$(mktemp -d) && git clone --depth=1 https://github.com/openclaw/skills "$T" && mkdir -p ~/.openclaw/skills && cp -r "$T/skills/biao29/til" ~/.openclaw/skills/openclaw-skills-til && rm -rf "$T"
skills/biao29/til/SKILL.mdtil
Capture and manage "Today I Learned" entries on OpenTIL -- from drafting to publishing, all within the CLI.
Setup
- Go to https://opentil.ai/dashboard/settings/tokens and create a Personal Access Token with
,read:entries
, andwrite:entries
scopesdelete:entries - Copy the token (starts with
)til_ - Set the environment variable:
export OPENTIL_TOKEN="til_xxx"
Token Resolution
Token resolution order:
environment variable (overrides all profiles)$OPENTIL_TOKEN
file — active profile's token (created by~/.til/credentials
)/til auth
If neither is set, entries are saved locally to
~/.til/drafts/.
Credential File Format
~/.til/credentials stores named profiles in YAML:
active: personal profiles: personal: token: til_abc... nickname: hong site_url: https://opentil.ai/@hong host: https://opentil.ai work: token: til_xyz... nickname: hong-corp site_url: https://opentil.ai/@hong-corp host: https://opentil.ai
: name of the currently active profileactive
: map of profile name → credentialsprofiles- Each profile stores:
,token
(from API),nickname
,site_urlhost
Backward compatibility: If
~/.til/credentials contains a plain text token (old format), silently migrate it to a default profile in YAML format and write back.
Subcommand Routing
The first word after
/til determines the action. Reserved words route to management subcommands; anything else is treated as content to capture.
| Invocation | Action |
|---|---|
| List entries (default: drafts) |
| Publish an entry |
| Unpublish (revert to draft) |
| AI-assisted edit |
| Search entries by title |
| Delete entry (with confirmation) |
| Show site status and connection info |
| Sync local drafts to OpenTIL |
| List site tags with usage counts |
| List site categories |
| Batch-capture multiple TIL entries |
| Connect OpenTIL account (browser auth) |
| Switch active profile (by profile name or @nickname) |
| List all profiles |
| Remove a profile |
| Rename a profile |
| Capture content as a new TIL |
| Extract insights from conversation (multi-candidate) |
Reserved words:
list, publish, unpublish, edit, search, delete, status, sync, tags, categories, batch, auth.
Reference Loading
⚠️ DO NOT read reference files unless specified below. SKILL.md contains enough inline context for most operations.
On subcommand dispatch (load before execution):
| Subcommand | References to load |
|---|---|
| none |
(extract from conversation) | none |
| references/management.md |
| references/management.md |
| references/management.md, references/local-drafts.md |
| references/management.md, references/api.md |
| references/management.md |
On-demand (load only when the situation arises):
| Trigger | Reference to load |
|---|---|
| API returns non-2xx after inline error handling is insufficient | references/api.md |
| Auto-detection context (proactive TIL suggestion) | references/auto-detection.md |
| No token found (first-run local fallback) | references/local-drafts.md |
API Quick Reference
Create and publish an entry:
curl -X POST "https://opentil.ai/api/v1/entries" \ -H "Authorization: Bearer $OPENTIL_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "entry": { "title": "Go interfaces are satisfied implicitly", "content": "In Go, a type implements an interface...", "summary": "Go types implement interfaces implicitly by implementing their methods, with no explicit declaration needed.", "tag_names": ["go", "interfaces"], "published": true, "lang": "en" } }'
Key create parameters:
| Field | Type | Required | Description |
|---|---|---|---|
| string | yes | Markdown body (max 100,000 chars) |
| string | no | Entry title (max 200 chars). Auto-generates slug. |
| array | no | 1-3 lowercase tags, e.g. |
| boolean | no | for draft (default), to publish immediately |
| string | no | Language code: , , , , , etc. |
| string | no | Custom URL slug. Auto-generated from title if omitted. |
| string | no | (default), , or |
| string | no | AI-generated summary for listing pages (max 500 chars) |
Management endpoints:
| Endpoint | Method | Description |
|---|---|---|
| GET | List/search entries |
| GET | Get a single entry |
| PATCH | Update entry fields |
| DELETE | Permanently delete entry |
| POST | Publish a draft |
| POST | Revert to draft |
| GET | Site info (username, entry counts, etc.) |
| GET | List tags with usage counts |
| GET | List categories with entry counts |
Full parameter list, response format, and error handling: see references/api.md
Execution Flow
Every
/til invocation follows this flow:
- Generate -- craft the TIL entry (title, body, summary, tags, lang)
- Check token -- resolve token (env var → active profile in
)~/.til/credentials- If
exists in old plain-text format, migrate to YAML~/.til/credentials
profile firstdefault - Found -> POST to API with
-> show published URLpublished: true - Not found -> save to
-> show first-run guide with connect prompt~/.til/drafts/ - 401 response -> save locally -> inline re-authentication (see Error Handling):
- Token from
(active profile) or no prior token: prompt to reconnect via device flow → on success, update the active profile's token and auto-retry the original operation~/.til/credentials - Token from
env var: cannot auto-fix — guide user to update/unset the variable$OPENTIL_TOKEN
- Token from
- If
- Show identity -- when ≥2 profiles are configured, include
in result messages so the user always knows which account was usedAccount: @nickname (profile_name) - Never lose content -- the entry is always persisted somewhere
- On API failure -> save locally as draft (fallback unchanged)
/til <content>
-- Explicit Capture
/til <content>The user's input is raw material -- a seed, not the final entry. Generate a complete TIL from it:
- Short input (a sentence or phrase) -> expand into a full entry with context and examples
- Long input (a paragraph or more) -> refine and structure, but preserve the user's intent
Steps:
- Treat the user's input as a seed -- craft a complete title + body from it
- Generate a concise title (5-15 words) in the same language as the content
- Write a self-contained Markdown body (see Content Guidelines below)
- Generate a summary (see Summary Guidelines below)
- Infer 1-3 lowercase tags from technical domain (e.g.
,rails
,postgresql
)go - Detect language -> set
(lang
,en
,zh-CN
,zh-TW
,ja
,ko
,es
,fr
,de
,pt-BR
,pt
,ru
,ar
,bs
,da
,nb
,pl
,th
,tr
)it - Follow Execution Flow above (check token -> POST or save locally)
No confirmation needed -- the user explicitly asked to capture. Execute directly.
/til
-- Extract from Conversation
/tilWhen
/til is used without arguments, analyze the current conversation for learnable insights.
Steps:
- Scan the conversation for knowledge worth preserving -- surprising facts, useful techniques, debugging breakthroughs, "aha" moments
- Identify all TIL-worthy insights (not just one), up to 5
- Branch based on count:
0 insights:
No clear TIL insights found in this conversation.
1 insight: Generate the full draft (title, body, tags), show it, ask for confirmation. On confirmation -> follow Execution Flow.
2+ insights: Show a numbered list (max 5), let the user choose:
Found 3 TIL-worthy insights: 1. Go interfaces are satisfied implicitly 2. PostgreSQL JSONB arrays don't support GIN @> 3. CSS :has() enables parent selection Which to capture? (1/2/3/all/none)
- Single number -> generate draft for that insight, show confirmation, proceed
- Comma-separated list (e.g.
) -> generate drafts for selected, show all for confirmation, POST sequentially1,3
-> generate drafts for each, show all for confirmation, POST sequentiallyall
-> cancelnone
- For each selected insight, generate a standalone TIL entry following Content Guidelines
- Show the generated entry to the user and ask for confirmation before proceeding
- On confirmation -> follow Execution Flow above (check token -> POST or save locally)
Auto-Detection
When working alongside a user, proactively detect moments worth capturing as TIL entries.
When to Suggest
Suggest when the conversation produces a genuine "aha" moment — something surprising, non-obvious, or worth remembering. Examples:
- Debugging uncovered a non-obvious root cause
- A language/framework behavior contradicted common assumptions
- Refactoring revealed a clearly superior pattern
- Performance optimization yielded measurable improvement
- An obscure but useful tool flag or API parameter was discovered
- Two technologies interacting produced unexpected behavior
Do NOT suggest for: standard tool usage, documented behavior, typo-caused bugs, or widely known best practices.
Rate Limiting
- Once per session — after suggesting once (accepted or declined), never suggest again
- Natural pauses only — suggest at resolution points or task boundaries, never mid-problem-solving
- Respect rejection — if declined, move on without persuasion
Suggestion Format
Append at the end of your normal response. Never interrupt workflow.
Template:
💡 TIL: [concise title of the insight] Tags: [tag1, tag2] · Capture? (yes/no)
Example (at the end of a debugging response):
...so the fix is to close the channel before the goroutine exits. 💡 TIL: Unclosed Go channels in goroutines cause silent memory leaks Tags: go, concurrency · Capture? (yes/no)
Capture Flow
Auto-detected TILs bypass the extract flow. The suggestion itself is the candidate.
- User replies
/yes
/y
/ok
→ agent generates full entry (title, body, tags, lang) from the suggested insight → follows Execution Flow (POST or save locally)sure - User replies
/ ignores / continues other topic → move on, do not ask againno
Non-affirmative responses (continuing the conversation about something else) are treated as implicit decline.
Detailed trigger examples, state machine, and anti-patterns: see references/auto-detection.md
Management Subcommands
Management subcommands require a token. There is no local fallback -- management operations need the API.
/til list [drafts|published|all]
/til list [drafts|published|all]List entries. Default filter:
drafts.
- API:
GET /entries?status=<filter>&per_page=10 - Display as a compact table with short IDs (last 8 chars, prefixed with
)... - Show pagination info at the bottom
/til publish [<id> | last]
/til publish [<id> | last]Publish a draft entry.
resolves to the most recently created entry in this session (tracked vialast
set on every successful POST)last_created_entry_id- Fetch the entry first, show title/tags, ask for confirmation
- On success, display the published URL
- If already published, show informational message (not an error)
/til unpublish <id>
/til unpublish <id>Revert a published entry to draft.
- Fetch the entry first, confirm before unpublishing
- If already a draft, show informational message
/til edit <id> [instructions]
/til edit <id> [instructions]AI-assisted editing of an existing entry.
- Fetch the full entry via
GET /entries/:id - Apply changes based on instructions (or ask what to change if none given)
- Show a diff preview of proposed changes
- On confirmation,
with only the changed fieldsPATCH /entries/:id
/til search <keyword>
/til search <keyword>Search entries by title.
- API:
GET /entries?q=<keyword>&per_page=10 - Same compact table format as
list
/til delete <id>
/til delete <id>Permanently delete an entry.
- Fetch the entry, show title and status
- Double-confirm: "This cannot be undone. Type 'delete' to confirm."
- On confirmation,
DELETE /entries/:id
/til status
/til statusShow site status and connection info. Works without a token (degraded display).
- With token:
-> show username, entry breakdown (total/published/drafts), token status, local draft count, dashboard linkGET /site - Without token: show "not connected", local draft count, setup link
/til sync
/til syncExplicitly sync local drafts from
~/.til/drafts/ to OpenTIL. Requires token.
- List pending drafts, POST each one, delete local file on success
- Show summary with success/failure per draft
/til tags
/til tagsList site tags sorted by usage count (top 20). Requires token.
- API:
GET /tags?sort=popular&per_page=20&with_entries=true - Show as compact table with tag name and entry count
/til categories
/til categoriesList site categories. Requires token.
- API:
GET /categories - Show as compact table with name, entry count, and description
/til batch <topics>
/til batch <topics>Batch-capture multiple TIL entries in one invocation. Requires explicit topic list.
- User lists topics separated by newlines, semicolons, or markdown list items (
/-
)1. - Generate a draft for each -> show all drafts for confirmation -> POST sequentially
- On partial failure, show per-entry success/failure (same format as
)/til sync
ID Resolution
- In listings, show IDs in short form:
+ last 8 characters... - Accept both short and full IDs as input
- Resolve short IDs by suffix match against the current listing
- If ambiguous (multiple matches), ask for clarification
Session State
Track the following session state (not persisted across sessions):
-- set on every successfullast_created_entry_id
(201). Used byPOST /entries
./til publish last
-- the profile name resolved at first token access. Reflects theactive_profile
field fromactive
(or~/.til/credentials
override). Used for identity display and draft attribution.$OPENTIL_TOKEN
Detailed subcommand flows, display formats, and error handling: see references/management.md
Agent Identity
Three layers of attribution signal distinguish human-initiated from agent-initiated TILs.
Layer 1: HTTP Headers
Include these headers on every API call:
X-OpenTIL-Source: human | agent X-OpenTIL-Agent: <your agent display name> X-OpenTIL-Model: <human-readable model name>
- Source:
and/til <content>
->/til
; Auto-detected ->humanagent - Agent: use your tool's display name (e.g.
,Claude Code
,Cursor
). Do not use a slug.GitHub Copilot - Model: use a human-readable model name (e.g.
,Claude Opus 4.6
,GPT-4o
). Do not use a model ID.Gemini 2.5 Pro - Agent and Model are optional -- omit them if you are unsure.
Layer 2: Tag Convention
- Auto-detected TILs: automatically add
to the tag listagent-assisted
and/til <content>
: do not add the tag (unless the Agent substantially rewrote the content)/til
Layer 3: Attribution Rendering (Backend)
Agent-initiated TILs are visually marked on OpenTIL automatically based on the
source field. No content modification needed -- the backend renders attribution
in the display layer.
- Public page: shows
, or✨ via {agent_name}
when agent_name is absent✨ AI - Tooltip (hover): shows
when both are present{agent_name} · {model} - Dashboard: shows ✨ badge + agent_name, or "Agent" when agent_name is absent
Do NOT append any footer or attribution text to the content body.
Summary
| Dimension | | | Auto-detected |
|---|---|---|---|
| Trigger | User explicit | User command | Agent proactive |
| Confirmations | 0 (direct publish) | 1 (review before publish) | 1 (suggest → capture) |
| Source header | | | |
| Agent header | Yes | Yes | Yes |
| Model header | Yes | Yes | Yes |
tag | No | No | Yes |
| Attribution | Automatic (backend) | Automatic (backend) | Automatic (backend) |
Content Guidelines
Every TIL entry must follow these rules:
- Self-contained: The reader must understand the entry without any conversation context. Never write "as we discussed", "the above error", "this project's config", etc.
- Desensitized: Remove project names, company details, colleague names, internal URLs, and proprietary business logic. Generalize specifics: "our User model" -> "a model", "the production server" -> "a production environment", "the Acme payment service" -> "a payment gateway".
- Universally valuable: Write to StackOverflow-answer standards. A stranger searching for this topic should find the entry immediately useful. Content only useful to the author belongs in private notes, not TIL.
- Factual tone: State facts, show examples, explain why. Avoid first-person narrative ("I was debugging...", "I discovered..."). Exception: brief situational context is fine ("When upgrading Rails from 7.2 to 8.0...").
- One insight per entry: Each TIL teaches exactly ONE thing. If there are multiple insights, create separate entries.
- Concrete examples: Include code snippets, commands, or specific data whenever relevant. Avoid vague descriptions.
- Title: 5-15 words. Descriptive, same language as content. No "TIL:" prefix.
- Content: Use the most efficient format for the knowledge — tables for comparisons, code blocks for examples, lists for enumerations, math (
/$inline$
) for formulas with fractions/subscripts/superscripts/greek letters, Mermaid diagrams ($$display$$
) for flows/states/sequences that text cannot clearly express. Simple expressions like```mermaid
stay as inline code; use math only when notation complexity warrants it. Only use prose when explaining causation or context. Never pad content; if one sentence suffices, don't write a paragraph.O(n) - Tags: 1-3 lowercase tags from the technical domain (
,go
,rails
,postgresql
,css
). No generic tags likelinux
orprogramming
.til - Lang: Detect from content. Chinese ->
, Traditional Chinese ->zh-CN
, English ->zh-TW
, Japanese ->en
, Korean ->ja
.ko - Category: Do not auto-infer
-- only include it if the user explicitly specifies a category/topic.category_name - Summary: 1-2 sentences, plain text (no markdown). Max 500 chars and must be shorter than the content body. Same language as content. Self-contained: the reader should understand the core takeaway from the summary alone. Be specific about what the reader will learn, not meta ("this article discusses..."). No first person, no meta-descriptions. Omit if the content is already very short (under ~200 chars) -- the excerpt fallback is sufficient.
Result Messages
API Success (token configured, 201)
Published to OpenTIL Title: Go interfaces are satisfied implicitly Tags: go, interfaces URL: https://opentil.ai/@username/go-interfaces-are-satisfied-implicitly
When ≥2 profiles are configured, add an
Account line:
Published to OpenTIL Account: @hong (personal) Title: Go interfaces are satisfied implicitly Tags: go, interfaces URL: https://opentil.ai/@hong/go-interfaces-are-satisfied-implicitly
Single-profile users see no
Account line — keep the output clean.
Extract the
url field from the API response for the URL.
Sync Local Drafts
After the first successful API call, check
~/.til/drafts/ for pending files. If any exist, offer to sync:
Draft saved to OpenTIL Title: Go interfaces are satisfied implicitly Tags: go, interfaces Review: https://opentil.ai/@username/go-interfaces-are-satisfied-implicitly Found 3 local drafts from before. Sync them to OpenTIL?
On confirmation, POST each draft to the API. Delete the local file after each successful sync. Keep files that fail. Show summary:
Synced 3 local drafts to OpenTIL + Go defer runs in LIFO order + PostgreSQL JSONB indexes support GIN operators + CSS :has() selector enables parent selection
If the user declines, keep the local files and do not ask again in this session.
First Run (no token)
Save the draft locally, then proactively offer to connect. This is NOT an error -- the user successfully captured a TIL.
TIL captured Title: Go interfaces are satisfied implicitly Tags: go, interfaces File: ~/.til/drafts/20260210-143022-go-interfaces.md Connect to OpenTIL to publish entries online. Connect now? (y/n)
→ run inline device flow (same asy
) → on success, sync the just-saved draft + any other pending drafts in/til auth~/.til/drafts/
→ show manual setup instructions (see Manual Setup Instructions below)n
Only show the connect prompt on the first local save in this session. On subsequent saves, use the short form (no prompt):
TIL captured Title: Go interfaces are satisfied implicitly Tags: go, interfaces File: ~/.til/drafts/20260210-143022-go-interfaces.md
Error Handling
On ANY API failure, always save the draft locally first. Never let user content be lost.
422 -- Validation error: Analyze the error response, fix the issue (e.g. truncate title to 200 chars, correct lang code), and retry. Only save locally if the retry also fails.
401 -- Token invalid or expired (token from
active profile):~/.til/credentials
TIL captured (saved locally) File: ~/.til/drafts/20260210-143022-go-interfaces.md Token expired for @hong (personal). Reconnect now? (y/n)
→ run inline device flow (same asy
) → on success, update the active profile's token in/til auth
and auto-retry the original POST (publish the just-saved draft, then delete the local file)~/.til/credentials
→ show manual setup instructions (see Manual Setup Instructions below)n
When only one profile exists, omit the
@nickname (profile) from the message.
401 -- Token invalid or expired (token from
env var):$OPENTIL_TOKEN
The env var takes priority over
~/.til/credentials, so saving a new token via device flow would not help — the env var would still be used. Guide the user instead:
TIL captured (saved locally) File: ~/.til/drafts/20260210-143022-go-interfaces.md Your $OPENTIL_TOKEN is expired or invalid. To fix: • Update the variable with a new token, or • unset OPENTIL_TOKEN, then run /til auth Create a new token: https://opentil.ai/dashboard/settings/tokens
Network failure or 5xx:
TIL captured (saved locally -- API unavailable) File: ~/.til/drafts/20260210-143022-go-interfaces.md
Full error codes, 422 auto-fix logic, and rate limit details: see references/api.md
Re-authentication Safeguards
| Rule | Behavior |
|---|---|
| No retry loops | If re-auth succeeds but the retry still returns 401 → stop and show the error. Do not re-authenticate again. |
| Batch-aware | During batch/sync operations, re-authenticate at most once. On success, continue processing remaining items with the new token. |
| Respect refusal | If the user declines re-authentication (), do not prompt again for the rest of this session. Use the short local-save format silently. |
| Env var awareness | When the active token comes from , never attempt device flow — it cannot override the env var. Always show the env var guidance instead. |
| Profile-aware re-auth | On successful re-authentication, update the corresponding profile's token in . Do not create a new profile. |
Manual Setup Instructions
When the user declines inline authentication (answers
n), show:
Or set up manually: 1. Visit https://opentil.ai/dashboard/settings/tokens 2. Create a token (select read + write + delete scopes) 3. Add to shell profile: export OPENTIL_TOKEN="til_..."
Local Draft Fallback
When the API is unavailable or no token is configured, drafts are saved locally to
~/.til/drafts/.
File format:
YYYYMMDD-HHMMSS-<slug>.md
--- title: "Go interfaces are satisfied implicitly" tags: [go, interfaces] lang: en summary: "Go types implement interfaces implicitly by implementing their methods, with no explicit declaration needed." profile: personal --- In Go, a type implements an interface...
The
profile field records the active profile name at save time, ensuring sync uses the correct account's token. Omitted when no profiles are configured (backward-compatible).
Full directory structure, metadata fields, and sync protocol: see references/local-drafts.md
Notes
- UI language adaptation: All prompts, result messages, and error messages in this document are written in English as canonical examples. At runtime, adapt them to match the user's language in the current session (e.g. if the user writes in Chinese, display messages in Chinese). Entry content language (
field) is independent -- it is always detected from the content itself.lang - Entries are published immediately by default (
) -- usepublished: true
to revert to draft/til unpublish <id> - The API auto-generates a URL slug from the title
- Tags are created automatically if they don't exist on the site
- Content is rendered to HTML server-side (GFM Markdown with syntax highlighting, KaTeX math, and Mermaid diagrams)
- Management subcommands (
,list
,publish
,edit
,search
,delete
,tags
,categories
,sync
) require a token -- no local fallback. Exception:batch
andstatus
(includingauth
,auth switch
,auth list
,auth remove
) work without a token.auth rename - Scope errors map to specific scopes:
/list
/search
/tags
needcategories
,read:entries
/publish
/unpublish
/edit
/sync
needbatch
,write:entries
needsdelete
.delete:entries
usesstatus
when available but works without a token.read:entries