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
آخر نشاط في المصدر
٢٧ سبتمبر ٢٠٢٦ في ٠٠:١٢
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٠
التفرعات
٠

خيارات التثبيت

يُحدَّد 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