til
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 , , and scopes
- Copy the token (starts with )
- Set the environment variable:
bash
export OPENTIL_TOKEN="til_xxx"
Token Resolution
Token resolution order:
- environment variable
- file (created by )
If neither is set, entries are saved locally to
.
Subcommand Routing
The first word after
determines the action. Reserved words route to management subcommands; anything else is treated as content to capture.
| Invocation | Action |
|---|
/til list [drafts|published|all]
| List entries (default: drafts) |
/til publish [<id> | last]
| Publish an entry |
| Unpublish (revert to draft) |
/til edit <id> [instructions]
| 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) |
| Capture content as a new TIL |
| Extract insights from conversation (multi-candidate) |
Reserved words:
,
,
,
,
,
,
,
,
,
,
,
.
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 |
/til list|status|tags|categories
| references/management.md |
/til publish|unpublish|edit|search|delete|batch
| references/management.md |
| references/management.md, references/local-drafts.md |
| references/management.md, references/api.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:
bash
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...",
"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 |
Management endpoints:
| Endpoint | Method | Description |
|---|
/entries?status=draft&q=keyword
| 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
invocation follows this flow:
- Generate -- craft the TIL entry (title, body, tags, lang)
- Check token -- resolve token (env var → )
- Found -> POST to API with -> show published URL
- Not found -> save to -> show first-run setup guide
- Never lose content -- the entry is always persisted somewhere
- On API failure -> save locally as draft (fallback unchanged)
-- Explicit Capture
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)
- Infer 1-3 lowercase tags from technical domain (e.g. , , )
- Detect language -> set (, , , , , , , , , , , , , , , , , , )
- Follow Execution Flow above (check token -> POST or save locally)
No confirmation needed -- the user explicitly asked to capture. Execute directly.
-- Extract from Conversation
When
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 sequentially
- -> generate drafts for each, show all for confirmation, POST sequentially
- -> cancel
- 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 / / / → agent generates full entry (title, body, tags, lang) from the suggested insight → follows Execution Flow (POST or save locally)
- User replies / ignores / continues other topic → move on, do not ask again
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]
List entries. Default filter:
.
- 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]
Publish a draft entry.
- resolves to the most recently created entry in this session (tracked via set on every successful POST)
- 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)
Revert a published entry to draft.
- Fetch the entry first, confirm before unpublishing
- If already a draft, show informational message
/til edit <id> [instructions]
AI-assisted editing of an existing entry.
- Fetch the full entry via
- 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 fields
Search entries by title.
- API:
GET /entries?q=<keyword>&per_page=10
- Same compact table format as
Permanently delete an entry.
- Fetch the entry, show title and status
- Double-confirm: "This cannot be undone. Type 'delete' to confirm."
- On confirmation,
Show 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 link
- Without token: show "not connected", local draft count, setup link
Explicitly sync local drafts from
to OpenTIL. Requires token.
- List pending drafts, POST each one, delete local file on success
- Show summary with success/failure per draft
List 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
List site categories. Requires token.
- API:
- Show as compact table with name, entry count, and description
Batch-capture multiple TIL entries in one invocation. Requires explicit topic list.
- User lists topics separated by newlines, semicolons, or markdown list items ( / )
- Generate a draft for each -> show all drafts for confirmation -> POST sequentially
- On partial failure, show per-entry success/failure (same format as )
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
-- set on every successful
(201). Used by
. Not persisted across sessions.
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 -> ; Auto-detected ->
- Agent: use your tool's display name (e.g. , , ). Do not use a slug.
- Model: use a human-readable model name (e.g. , , ). Do not use a model ID.
- Agent and Model are optional -- omit them if you are unsure.
Layer 2: Tag Convention
- Auto-detected TILs: automatically add to the tag list
- and : do not add the tag (unless the Agent substantially rewrote the content)
Layer 3: Attribution Rendering (Backend)
Agent-initiated TILs are visually marked on OpenTIL automatically based on the
field. No content modification needed -- the backend renders attribution
in the display layer.
- Public page: shows , or when agent_name is absent
- Tooltip (hover): shows when both are present
- 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 ( / ) for formulas with fractions/subscripts/superscripts/greek letters, Mermaid diagrams () for flows/states/sequences that text cannot clearly express. Simple expressions like 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.
- Tags: 1-3 lowercase tags from the technical domain (, , , , ). No generic tags like or .
- Lang: Detect from content. Chinese -> , Traditional Chinese -> , English -> , Japanese -> , Korean -> .
- Category: Do not auto-infer -- only include it if the user explicitly specifies a category/topic.
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
Extract the
field from the API response for the URL.
Sync Local Drafts
After the first successful API call, check
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 show a concise setup hint. 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
Sync to OpenTIL? Run: /til auth
Only show the setup hint on the first local save in this session. On subsequent saves, use the short form:
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:
TIL captured (saved locally -- token expired)
File: ~/.til/drafts/20260210-143022-go-interfaces.md
Token expired. Run /til auth to reconnect.
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
Local Draft Fallback
When the API is unavailable or no token is configured, drafts are saved locally to
.
File format: YYYYMMDD-HHMMSS-<slug>.md
markdown
---
title: "Go interfaces are satisfied implicitly"
tags: [go, interfaces]
lang: en
---
In Go, a type implements an interface...
Full directory structure, metadata fields, and sync protocol: see references/local-drafts.md
Notes
- Entries are published immediately by default () -- use to revert to draft
- 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 (, , , , , , , , ) require a token -- no local fallback. Exception: and work without a token.
- Scope errors map to specific scopes: /// need , //// need , needs . uses when available but works without a token.