| name | create-card |
| description | Create a new HashDo card. Use when the user wants to build a new card, asks "create a card", or says "/create-card". Guides scaffolding, implementation, and building. |
| argument-hint | <card-name> [description] |
HashDo Card Creation Guide
You are creating a new HashDo v2 card. Follow this process exactly.
Step 1: Gather Requirements
Before writing any code, confirm with the user:
- Card name — must be kebab-case, prefixed with
do- (e.g. do-weather, do-recipe, do-game-chess)
- What it does — one sentence: what data does it show? What API(s) does it call?
- Inputs — what parameters does the user provide? Which are optional with sensible defaults?
- Actions — any interactive state mutations? (favorites, toggles, votes, etc.)
- Shareable? — should users be able to share a link to a specific card instance?
- Unique instances? — should each invocation create a new instance even with the same inputs? (e.g. polls, games). If yes, set
uniqueInstance: true and add an id input.
- Category — is this a top-level card (
demo-cards/<name>/) or nested under a category (demo-cards/game/<name>/)?
If the user provides a card name and description, infer reasonable answers for the rest and confirm before proceeding.
Step 2: Scaffold Files
Every card needs exactly 3 files. Create them in the correct location:
For top-level cards:
v2/demo-cards/<name>/
card.ts # Card definition (the only source file)
package.json # Package metadata
tsconfig.json # TypeScript config
For nested/category cards (e.g. games):
v2/demo-cards/<category>/<name>/
card.ts
package.json
tsconfig.json
package.json template:
{
"name": "@hashdo/card-<name>",
"version": "1.0.0",
"description": "<one-line description>",
"type": "module",
"main": "card.js",
"scripts": {
"build": "tsc"
},
"devDependencies": {
"typescript": "^5.5.0",
"@hashdo/core": "*"
},
"license": "MIT"
}
For nested cards, the package name should include the category: @hashdo/card-<category>-<name> (e.g. @hashdo/card-game-chess).
tsconfig.json (always identical):
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["card.ts"]
}
Step 3: Write card.ts
Use this exact structure. Every card follows the same pattern:
import { defineCard, colors, gradients, categoricalColors } from '@hashdo/core';
export default defineCard({
name: 'do-<name>',
description:
'<LLM-optimized description. Be specific about when to use this tool. ' +
'End with: All parameters have defaults — call this tool immediately without asking the user for parameters.>',
shareable: true,
inputs: {
exampleInput: {
type: 'string',
required: false,
default: 'sensible default value',
description: 'Clear description. Mention the default behavior when omitted.',
},
},
async getData({ inputs, rawInputs, state, baseUrl, userId }) {
let textOutput = `## Title\n\n`;
textOutput += `| | |\n|---|---|\n`;
textOutput += `| Key | Value |\n`;
const viewModel = {
};
return {
viewModel,
textOutput,
state: {
...state,
lastChecked: new Date().toISOString(),
},
};
},
actions: {
exampleAction: {
label: 'Button Label',
description: 'What this action does (for LLM docs)',
async handler({ cardInputs, actionInputs, state }) {
return {
state: { ...state, },
message: 'Action completed',
};
},
},
},
template: (vm) => `
<div style="font-family:system-ui,sans-serif; padding:20px; max-width:380px;
background:${gradients.purple}; border-radius:12px; color:white;">
<!-- Card HTML using viewModel values like ${`\${vm.field}`} -->
</div>
`,
});
Key Rules
Naming
- Card name:
do-<name> (kebab-case, e.g. do-weather, do-game-wordle)
- Tag:
#do/<name> (used in the JSDoc comment)
- Package:
@hashdo/card-<name>
Inputs
- Always provide defaults — cards should render immediately without user input
- Use
required: false with default values whenever possible
- Write descriptions that help LLMs understand when/how to set the parameter
- Use
enum with as const to constrain string values
Description
- The
description field is critical — LLMs use it to decide when to call the tool
- Be specific: mention trigger phrases like "when the user asks about X"
- End with encouragement to call immediately without prompting for params
getData
- Numbered comment sections (── 1. ..., ── 2. ...) for readability
- Throw descriptive
Error objects on failure
- Always return
textOutput (markdown) for chat-based AI clients
- Always return
viewModel for HTML rendering
- Spread
...state when adding to existing state to preserve other keys
Actions
- Use for stateful user interactions (favorites, toggles, votes)
- Keep handlers pure: read state, return new state
- Return a
message string for user feedback
- For user-specific state, set
stateKey: (_inputs, userId) => userId ? \user:${\${userId}}` : undefined`
Template
- Use inline
template: (vm) => \...`` for self-contained cards
- Style with inline CSS (no external stylesheets)
- Target
max-width: 320-400px for card width
- Use
border-radius: 12-20px for the outer container
- Use
system-ui, sans-serif font stack
- Design for both light and dark backgrounds
Color Palette
Cards must use the shared color palette from @hashdo/core. Never hardcode ad-hoc hex colors.
import { defineCard, colors, categoricalColors, gradients } from '@hashdo/core';
9 color ramps — each with 7 stops (50=lightest → 900=darkest):
purple, teal, coral, pink, gray, blue, green, amber, red
| Stop | Use |
|---|
| 50 | Lightest fill (card backgrounds, badges) |
| 100-200 | Light fills, hover states |
| 400 | Mid tone (icons, accents, chart series) |
| 600 | Strong (borders, strokes, subtitles on light fills) |
| 800-900 | Text on light fills, darkest shade |
Usage:
colors.purple[50]
colors.purple[400]
colors.purple[800]
colors.positive
colors.negative
Header gradients — use gradients.<ramp> for card headers:
gradients.purple
gradients.teal
Categorical coloring (poll options, chart series) — use categoricalColors:
categoricalColors[i % categoricalColors.length]
Rules:
- Color encodes meaning, not sequence. Group by category, not by order.
- Use 2-3 ramps per card, not 6+.
- Prefer
purple, teal, coral, pink for general categories.
- Reserve
blue, green, amber, red for semantic meaning (info, success, warning, error).
- Text on colored backgrounds: use the 800/900 stop from the same ramp as the fill.
- Positive/negative indicators:
colors.positive / colors.negative.
State & Instances
CardState is Record<string, unknown> — arbitrary JSON
- Every render produces an instanceId — a short, deterministic, URL-safe identifier
- State persists per instance via
cardKey = card:{name}:{stateKey || hash}
- Always spread existing state:
{ ...state, newField: value }
- Set
uniqueInstance: true + an id input when each invocation should create a new instance (polls, games). The system auto-generates id via prepareInputs() when not provided.
- For per-user state, use
stateKey: (_inputs, userId) => userId ? \user:${\${userId}}` : undefined`
Error Handling
- Throw
new Error('descriptive message') from getData — the framework renders an error card
- Log errors with
console.error([] ${detail}) before throwing
- Validate API responses before accessing nested properties
Step 4: Build & Test
After creating the card files:
-
Build the card:
cd v2/demo-cards/<name> && npx tsc
-
Build all packages (if dependencies changed):
cd v2 && npm run build
-
Start the dev server:
cd v2 && npm start
-
Test via REST API:
- Card list:
GET http://localhost:3000/api/cards
- Render:
GET http://localhost:3000/api/cards/do-<name>?<params>
- Screenshot:
GET http://localhost:3000/api/cards/do-<name>/image?<params>
-
Test via MCP: Connect Claude Desktop or any MCP client to http://localhost:3000/mcp
Common Patterns (from existing cards)
IP geolocation fallback (weather):
When location isn't provided, fall back to IP-based geolocation using http://ip-api.com/json/.
Watchlist / favorites:
Store an array in state, toggle membership in an action handler. Return count in message.
Accent colors by category:
Map a data field (subject, language, type) to gradient colors for visual variety.
Unique instances (poll, game):
Set uniqueInstance: true and add an id input. The system auto-generates a random 6-char hex id for new instances. Users can reopen an existing instance by passing the id directly. Define stateKey to use the id:
uniqueInstance: true,
inputs: { id: { type: 'string', required: false, description: 'Instance ID. Omit to create new.' }, ... },
stateKey: (inputs) => inputs.id ? `id:${inputs.id}` : undefined,
User-specific state (book, reading list):
Set stateKey: (_inputs, userId) => userId ? \user:${\${userId}}` : undefined` to isolate state per anonymous user.
Checklist
Before marking the card complete, verify: