Skip to main content

jig-writing

How to write jigs using the Jig SDK. Use when creating new jigs, refactoring existing ones, or deciding how to structure a workflow. Covers jig(), llm(), agent(), connections, and the core principle of compiling what you can while keeping agent() for fuzzy judgment.

소스 정보

저장소
agamm/jig
최근 소스 활동
2026년 9월 27일 00:12
감지된 SKILL.md 언어
영어
스타
0
포크
0

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
jig-writing
description
How to write jigs using the Jig SDK. Use when creating new jigs, refactoring existing ones, or deciding how to structure a workflow. Covers jig(), llm(), agent(), connections, and the core principle of compiling what you can while keeping agent() for fuzzy judgment.
# Writing Jigs Jigs are deterministic units of work with LLM "hatches" — points where intelligence is needed for content generation, judgment, or fuzzy data gathering. Everything else is code. ``` event → code → [agent hatch] → code → [llm hatch] → code → result fuzzy content deterministic ``` The goal: minimize what the LLM does at runtime. If you can hardcode it, hardcode it. ## SDK ### `jig(name, options, handler)` Define a jig. The `tools` array is the permission boundary — nothing outside it can be called. ```typescript import { jig, llm, agent } from "@jig/sdk" import { granola } from "@jig/connections/granola.js" import { workspace } from "@jig/connections/workspace.js" const myJig = jig("my-jig", { tools: [granola.list_meetings, workspace.gmail_createDraft], }, async (ctx) => { const company = "Acme" const recipient = "client@example.com" // ... }) export default myJig ``` Options: `trigger` (required, see rule 8), `tools`, and three optional runtime settings that only exist in the source: `model` (OpenRouter id, the default for every `llm()`/`agent()` in the jig), `runTimeoutMs` (whole-run watchdog, default 30 minutes) and `toolTimeoutMs` (ceiling per MCP tool call, default 5 minutes). See rule 12b for precedence. If a jig does not need user-supplied inputs, omit `params` entirely. Do not add placeholder or speculative params just because a jig could theoretically be configurable. ### `run(definition, params)` — called by the CLI, not by jig files Jig files just `export default myJig`. The CLI imports and calls `run()`. ```typescript // In jig files: just export export default myJig // The CLI handles execution, param prompting, dry-run, etc. ``` ### `llm(prompt, data, options?)` Content generation or judgment. No tool access. Deterministic for the same input. ```typescript // Plain text const email = await llm("Write a summary", { meetings, commits }) as string // Structured output const result = await llm("Classify this", { text }, { schema: { priority: "string", urgent: "boolean" } }) // → { priority: "high", urgent: true } // Lists of objects: one-element array declares the item shape const review = await llm("Review this", { text }, { schema: { score: "number", notes: [{ title: "string", detail: "string" }], } }) ``` Schema values are JSON Schema type names (`string`, `number`, `integer`, `boolean`, `object`, `array`, `null`), a nested object, or a one-element array. There is no `"any"`: providers run these in strict mode and reject it with a 400 *after* the model has generated the answer, so the work is done and discarded. Describe the real shape instead. Other options: `model` (OpenRouter id for this one call; wins over the step and jig models, see rule 12b) and `maxTokens` (default 64000; reasoning models spend it thinking before they answer. A reply left empty by an exhausted budget is retried once with 3x the budget before the step fails). ### `agent(prompt, tools, options?)` LLM with bounded tool calling. The agent decides which tools to call, how many times, and in what order, but only from the tools you give it. ```typescript const data = await agent( "Find all meetings and emails about this project from last week", [granola.query_granola_meetings, workspace.gmail_search, workspace.gmail_get] ) ``` Options match `llm()`: `schema`, `model` (this call only) and `maxTokens`. This is the "bigger hatch" — use it when the task requires judgment about *how* to gather or process data. The agent might search, read results, search again with a refined query, etc. ### `ctx.output(...args)` Write output. Never use `console.log()` in jig handlers — always `ctx.output()`. Output is captured by the runtime for dry-run, dashboard display, and testing. ```typescript ctx.output(result.email) ctx.output(`Draft: https://mail.google.com/mail/u/0/#drafts/${id}`) ``` ### `ctx.memory` — remember things across runs A persistent per-jig key/value store. Scoped to the jig: one jig can neither read nor overwrite another's keys. Values round-trip as JSON. ```typescript await ctx.memory.set("todo:42", { title: "Renew passport", dueAt }) const item = await ctx.memory.get<Todo>("todo:42") // null if never written const open = await ctx.memory.list<Todo>("todo:") // [{ key, value }], key order await ctx.memory.delete("todo:42") // true if one was there ``` Use it for anything the jig must know that its own last run learned: a to-do list, ids already processed, a running tally, the last cursor fetched. It is NOT for data you can re-fetch from a connection — read that live instead. Namespace keys with a prefix (`"todo:"`, `"seen:"`) so `list(prefix)` gives you one logical collection back. A jig storing records should key them by a stable id from the source, not by an incrementing counter. Limits: 64KB per value, 1000 keys per jig. A jig that writes a *new* key every run will eventually hit the cap — that is the signal to key by something stable or delete what is finished. Writes no-op during a dry run; reads return real data so the preview reflects true state. ### `ctx.once(key, fn)` - do something exactly once Runs `fn` the first time this jig sees `key`, and never again. ```typescript for (const meeting of meetings) { await ctx.once(`coached:${meeting.id}`, async () => { const transcript = await granola.get_meeting_transcript({ meeting_id: meeting.id }) await ctx.email({ subject: `Notes: ${meeting.title}`, text: await llm("Coach me", { transcript }) as string }) }) } ``` This is what makes a polling jig safe. A jig on a 15-minute cron sees the same item on every tick. Without a record of what it already handled it acts on each one over and over, and the user gets the same email four times an hour. **Prefer this over a time window whenever the source has stable ids.** Rule 12's window trick (match each item to exactly one cron bucket) is the fallback for when it does not: a window wide enough to survive a late tick is also wide enough to fire twice. Some sources have no end time to build a window from at all, and there `ctx.once` is the only option. The key is recorded *before* `fn` runs, so a crash midway costs one missed item rather than repeating a side effect that already happened. If `fn` throws, the key is released and the next run retries it. Returns true when `fn` ran, false when the key had been seen before. During a dry run `fn` runs and nothing is recorded. Keys count against the same 1000-key budget as `ctx.memory`, so scope them to something that ages out (`coached:<meeting-id>`), not something unbounded. ### `ctx.remind(at, payload, options?)` — wake this jig up later Schedules a one-off future run of this jig. Use it when the time to act is decided by the data, not by a fixed schedule: a to-do's due date, a follow-up in three days, a deferred retry. ```typescript await ctx.remind(dueAt, { title }, { key: "todo:42" }) const pending = await ctx.reminders() // [{ id, key, dueAt, payload }] await ctx.cancelReminder("todo:42") // true if one was pending ``` `at` takes a `Date`, epoch milliseconds, or an ISO string. **Always pass `options.key` when the reminder is about a specific thing.** With a key, re-reminding *replaces* that key's pending reminder instead of stacking a second one — so a jig that re-reads the same to-do on every run reschedules rather than sending a duplicate every time. Without a key, every call is a new independent reminder. When reminders come due, the scheduler starts the jig with them in `ctx.params.reminders` — always an **array**, because more than one can come due at once, and they arrive in one run rather than one run each: ```typescript const due = (ctx.params.reminders ?? []) as Array<{ key: string | null payload: unknown dueAt: string // ISO }> ``` So a jig that both accepts new items and fires reminders branches on which happened: ```typescript if (due.length > 0) { /* reminders fired */ } else if (ctx.params.email) { /* new item arrived */ } ``` Reminders fire for any trigger type, including `manual` — they are the only thing that wakes a manual jig on its own. A paused jig holds its reminders and delivers them when re-enabled rather than dropping them. `ctx.remind` no-ops during a dry run. Prefer this over a frequent cron that scans for due work: it fires at the exact time, and it does not spend 288 runs a day to send two reminders. ### `ctx.parallel(...promises)` Run multiple promises concurrently. Sugar over `Promise.all` with proper typing. ```typescript const [meetings, emails] = await ctx.parallel( granola.list_meetings({ time_range: "last_week" }), workspace.gmail_search({ query: company }), ) ``` ### Connections Import from `@jig/connections/`. Auto-generated after `jig connect <server>`. Tools connect lazily on first call — no setup code needed. ```typescript import { granola } from "@jig/connections/granola.js" import { workspace } from "@jig/connections/workspace.js" import { github } from "@jig/connections/github.js" ``` --- ## Jig Writing Rules (must follow) These rules are enforced by the runner and the static validator. Violations cause the jig to refuse to run with a clear error. ### 1. Maximize determinism (most important) Prefer direct tool calls > `llm()` > `agent()`: - Direct call when you know the tool + params at write time - `llm()` for synthesis, writing, or classification from known data - `agent()` only when the sequence of tool calls requires runtime judgment - Default to breaking large workflows into deterministic steps instead of one giant `agent()` ### 2. Structure with `ctx.step()` blocks (REQUIRED, enforced) **Every jig MUST wrap its work in at least one `ctx.step()` block.** Tool calls outside a step throw at runtime. Jigs with zero steps are rejected by the validator. `ctx.step()` is purely a labeled scope. **Do NOT assign its return value** — pass data between steps using `let` variables in the outer handler scope. ```typescript let events: any[] = [] await ctx.step("Get calendar events", [calendar_listEvents], async () => { events = await calendar_listEvents({...}) ctx.output(`Found ${events.length} events`) }) await ctx.step("Send Telegram", [telegram_send_message], async () => { await telegram_send_message({ chat_id: CHAT_ID, text: format(events) }) }) ``` Rules: - Wrap every logical group of tool calls in a `ctx.step()` block - Each step declares exactly which tools it uses — only those tools work inside the block - Every connection tool used inside a step callback MUST appear in that same step's tools array. If you call `apify.call_actor` and then `apify.get_actor_output`, either declare both tools in that step or, preferably, split them into two sequential steps. - If you violate the step tool allowlist, runtime fails with an error like: `Tool "apify.get-actor-output" is not allowed in step "Scrape GitHub trending repos". Declare it in the tools array of your ctx.step() call.` - **Steps MUST be flat — never nest `ctx.step()` inside another `ctx.step()` callback. The runtime AND the static validator throw if you do.** - Conditional early-return is fine, but the early-return path must NOT contain another `ctx.step()` — finish the current step, then branch in the outer handler. - `agent()` and `llm()` calls are always allowed inside any step — they don't need to be in the tools array - Step labels MUST be static strings — never use template literals with runtime variables. Good: `"Research meeting context"`. Bad: `` `Research: ${eventSummary}` `` - ALL `ctx.output()` calls must be inside `ctx.step()` blocks — output is tied to the step it belongs to - Use `ctx.output()` inside steps to show progress ### 3. Do NOT declare jig params - Do NOT declare `options.params` in a jig. Human-configured jig params are no longer supported. - If the request already specifies the value, hardcode it. - If you need a constant that the user did not provide, ask the user, then hardcode their answer. - `ctx.params` still exists at runtime for incoming webhook/manual payloads passed in externally. Use it only for intrinsic runtime payload data, not for declared jig configuration. ### 4. Hardcode constants, don't discover them at runtime - If the jig needs a value that is constant across runs (the user's email, name, team, Slack channel, timezone, recipient list, etc.), prefer hardcoding it over discovering it at runtime - If you don't know a constant, ask the user before writing code. Keep the question short: "What email should I send the briefing to?" Then hardcode their answer. - Do not turn those constants into jig params - When an LLM analyzes the user's own data (inbox, calendar, DMs, files), pass identity into the prompt (`"the user = <name> <email>"`) **and** filter self-originated records at the source (`-from:me`, organizer-equals-me, etc.) — otherwise the LLM will suggest you reply to your own sent mail ### 5. Use the right tools The available tools and probe results show what's available. Use multiple relevant tools when they materially improve the result. If the requested workflow depends on a specific connection, MCP server, or tool, the jig MUST actually use it. Do not degrade into a no-op jig that only explains setup steps. Forbidden patterns: - Do NOT tell the user to run `jig connect ...` from inside jig output - Do NOT emit placeholder copy like "Once connected, this jig will..." - Do NOT use `llm()` to fabricate "example output" for a tool you failed to call - Do NOT replace a missing integration with generic prose or mocked/sample results If the required connection or tool is unavailable, stop creation/editing and surface the missing dependency instead of writing placeholder code. ### 6. Preserve the existing toolset when editing - If you are editing an existing jig, do NOT add or remove tools unless the user explicitly asked for tool changes - Small logic, wording, output, or scheduling edits should usually keep the existing tools unchanged - Only change the toolset when the current tools are insufficient or invalid for the requested behavior ### 7. Edit existing jigs surgically When editing an existing jig, treat the current file as the source of truth. The user usually wants a narrow patch, not a rewrite. Before writing: - Identify the exact requested behavior change. - Locate the smallest code region that implements that behavior. - Preserve the existing trigger, schedule, recipients, subject lines, constants, data sources, step labels, step ordering, and output shape unless the user explicitly asks to change them. - Preserve existing formatting modes. If a jig sends HTML, keep sending HTML. If the user asks for HTML but the schema says the field is `isHtml`, use `isHtml: true` rather than inventing unsupported fields like `contentType`. - Preserve the current tool list unless Rule 6 says it must change. - Do not "simplify", collapse steps, remove data gathering, replace deterministic code with `llm()`/`agent()`, or remove user-visible sections just because they seem verbose. - Do not call `read_jig_file` if the current jig code is already in the prompt context. Use the provided code. - After the edit, run `check_jig`. If it fails, patch the narrow error and check again. Bad edit behavior: ```typescript // User asked: "make the email body HTML" // Bad: rewrite the whole jig, remove HTML conversion, shorten the workflow, // change the subject, and send plain text. ``` Good edit behavior: ```typescript // User asked: "make the email body HTML" const htmlBody = markdownToHtml(markdownBody) await workspace.gmail_send({ to: "user@example.com", subject, body: htmlBody, isHtml: true, }) ``` ### 8. Pick the right trigger (ASK if unclear) Every jig has exactly one `trigger` field: - `{ type: "manual" }` — user runs it from the CLI or dashboard button. No schedule, no external input. Use for one-off tasks, interactive workflows, or jigs triggered only by a human click. - `{ type: "cron", cron: "0 8 * * 1" }` — runs on a schedule (5-field cron: min hour day-of-month month day-of-week). Examples: `"0 8 * * 1"` = Mon 8am, `"*/15 * * * *"` = every 15 min, `"0 9 * * 1-5"` = weekdays 9am. Use for digests, reminders, periodic sync, reports. - `{ type: "webhook" }` — runs when an external service POSTs to `/api/webhooks/{jigId}?token=...`. The POST body becomes `ctx.params` (nested JSON). Use for incoming messages from Telegram, Slack events, GitHub webhooks, email inbound parse, etc. - `{ type: "email" }` — runs when the user emails the jig's own address. Use when the user wants to *send things to* a jig in plain English. See "Email trigger" below. - `{ type: "calendar", minutesBefore: N }` — runs once per upcoming meeting. See "Calendar trigger" below.
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기