# Jinn Jinn runs an AI agent on a task in a new Linux VM. You send a prompt and an input folder. The agent works until the files you asked for exist, then Jinn returns them as one `.tar.gz`. The VM is destroyed when the run ends. You define the task once as a **function**: the VM's setup, the model, the tools and the files to return. Each call of a function is a **run**. Start runs from the [CLI](/cli), the [API](/api), the [SDKs](/sdk) or the [console](/console). ## Start New to Jinn? Follow the [Quickstart](/quickstart): sign in, add a model key, publish a function and run it. ## Find what you need - [Copy an example](/examples): support tickets, pull request reviews, reports, web page checks. - [Write a function](/functions): every field of a definition, setup commands, providers, tools and secrets. - [Start and follow runs](/runs): states, failures, input and output folders, logs. - [Know the run's machine](/environment): sizes, what is installed, what the network allows, what is kept. - [Look up a limit](/limits): every size, count and duration in one table. - [Call the API](/api), or get results by [webhook](/webhooks). - [Use the CLI](/cli) or the [SDKs](/sdk). - [Use Jinn from an agent](/agents): JSON output, exit codes and error codes. ## For agents Every page is Markdown at the same address with `.md` added, for example [/runs.md](/runs.md), or with `Accept: text/markdown`. [llms.txt](/llms.txt) lists every page, [llms-full.txt](/llms-full.txt) holds them all, and [openapi.json](/openapi.json) describes the API. --- # Quickstart This takes about five minutes. You need an email address, a card for $5 of credit, and an API key from OpenAI, Anthropic or xAI. ## 1. Sign up and add credit Open [app.usejinn.com](https://app.usejinn.com) and enter your email address. Jinn emails you a code to enter. Name your account, then add $5 of credit under **Account**: runs start only while the account has credit. A run of the examples costs about a cent. ## 2. Install the CLI and sign it in ```sh $ curl -fsSL https://github.com/usejinn/jinn-cli/releases/latest/download/install.sh | sh jinn v0.4.0 is in /usr/local/bin/jinn. Next: jinn login $ jinn login Open https://app.usejinn.com/cli/login_8502eb454bbc39a9ec36b428 Check that it shows BDMT-FLXL, then approve. Signed in. The key is in /home/you/.config/jinn/key ``` Open the link, check that the console shows the same code, and approve. The CLI now has an API key for your account. ## 3. Add a provider A provider holds the model and your vendor key. The CLI reads the key from stdin, so it stays out of your shell history: ```sh $ echo "$OPENAI_API_KEY" | jinn provider openai --vendor openai --model gpt-5.2 --effort medium → openai prv_8c1f04d2a7b93e650f1a2c47 v1 · use prv_8c1f04d2a7b93e650f1a2c47@latest in a function's provider ``` ## 4. Get a function and publish it Download the [support ticket example](/examples#answer-a-support-ticket): a function that answers a billing ticket, a sample ticket and the billing policy. Its `function.json` names no provider, so it uses the one you just added, at its latest version. ```sh $ curl -sL https://docs.usejinn.com/examples/support-triage.tar.gz | tar -xz $ cd support-triage $ jinn publish function.json → support-triage fnc_a3b59df3be29992f250c2e1e v1 ``` ## 5. Run it `--in` sends the input folder; `--out` is where the files the run returns go. ```sh $ jinn run support-triage --prompt "Ticket #4812: charged twice" --in ./in --out ./result run_3b6e0e25251acd12d58a87f3 queued · support-triage v1 run_3b6e0e25251acd12d58a87f3 running agent read {"file_path":"/workspace/in/ticket.json","limit":null,"offset":null} agent read {"file_path":"/workspace/in/policy.md","limit":null,"offset":null} agent lookup_customer {"email":"maria.lopez@brightpath.io"} agent write {"file_path":"/workspace/out/actions.json","content":"[\n {\"charge\": \"ch_3Q8xK22\", \"amount\":… agent write {"file_path":"/workspace/out/reply.md","content":"Hi Maria,\n\nThanks for getting in touch, and so… agent submit_result ✓ succeeded in 17.6s result/actions.json 146 B result/reply.md 910 B $ cat result/actions.json [ {"charge": "ch_3Q8xK22", "amount": 348, "reason": "Duplicate charge: second Team plan October charge on 2026-10-01, duplicates ch_3Q8xK21"} ] ``` `result/reply.md` is the reply to send. The [examples page](/examples#answer-a-support-ticket) shows it, and how the agent got the customer's charges from a custom tool. ## Next - Run the other [examples](/examples): a pull request review, a weekly report and a phone check of a web page. - Add packages to the VM with [setup commands](/functions#bases-and-setup). - Start runs from your own code with the [API](/api) or an [SDK](/sdk). - Get each result posted to you with a [webhook](/webhooks). --- # Examples Each example is a folder you can download: the function, a sample input, and the output of a real run of it. To try one, you need only a Jinn sign-in and a model provider. ## Before you start Sign in the CLI and add a provider named `openai` with your own key, as in the [quickstart](/quickstart). The examples name no provider, so they use your account's only one, at its latest version: ```sh $ jinn login $ echo "$OPENAI_API_KEY" | jinn provider openai --vendor openai --model gpt-5.2 --effort medium → openai prv_8c1f04d2a7b93e650f1a2c47 v1 · use prv_8c1f04d2a7b93e650f1a2c47@latest in a function's provider ``` Any vendor and model works. If your account has more than one provider, add `"provider": "openai"` (or another name) before you publish. The runs below used Anthropic's `claude-opus-5-5` at medium effort. A model reads the same input differently each time, so your output says the same things in other words. Each folder holds: | Path | What it is | |---|---| | `function.json` | The function. Publish it once with `jinn publish`. | | `in/` | A sample input folder. `jinn run --in ./in` sends it. | | `out/` | What the run below returned, to compare with yours. | ## Answer a support ticket A customer writes that their card was charged twice. The agent reads the ticket and the billing policy, looks the customer up with a [custom tool](/functions#custom-tools), then returns a reply to send and the refunds to make. Your service sends the reply and makes the refunds. In production, call the run from your help desk's webhook and post `reply.md` when the [result arrives](/webhooks). ``` support-triage/ function.json in/policy.md in/ticket.json out/actions.json out/reply.md ``` ```json { "name": "support-triage", "system_prompt": "You answer billing tickets for Acme. /workspace/in holds the ticket (ticket.json) and the billing policy (policy.md). Look the customer up by the ticket's email with lookup_customer, and follow the policy exactly. Write the reply to the customer to /workspace/out/reply.md. Write the refunds to make to /workspace/out/actions.json as a JSON array of {\"charge\": id, \"amount\": number, \"reason\": text}; write [] if there are none. Then call submit_result.", "custom_tools": [{ "name": "lookup_customer", "description": "Find a customer by email: their plan, seats, prices and recent charges.", "parameters": { "type": "object", "properties": { "email": { "type": "string" } }, "required": ["email"], "additionalProperties": false }, "url": "https://docs.usejinn.com/tools/lookup-customer", "secret": "sk_example_support" }], "input_manifest": [ { "path": "ticket.json", "description": "The ticket" }, { "path": "policy.md", "description": "The billing policy" } ], "output_manifest": [ { "path": "reply.md", "description": "The reply to send" }, { "path": "actions.json", "description": "Refunds to make" } ] } ``` `lookup_customer` is an endpoint Jinn serves for this example, at `https://docs.usejinn.com/tools/lookup-customer`. It knows two made-up customers, and it answers only a call with the secret `sk_example_support`. When the agent calls the tool, Jinn sends its arguments to the endpoint: ```http POST https://docs.usejinn.com/tools/lookup-customer Authorization: Bearer sk_example_support Jinn-Run: run_3b6e0e25251acd12d58a87f3 {"email": "maria.lopez@brightpath.io"} ← 200 {"name": "Maria Lopez", "plan": "Team, monthly", "seats": 12, "charges": [ … ], …} ``` To use your own customer data, serve the same call from your service and put its address and secret in the tool. The secret is stored encrypted and never enters the VM. The input is the ticket and a four-line policy: ```json { "id": 4812, "from": "maria.lopez@brightpath.io", "subject": "Charged twice this month", "received_at": "2026-10-03T08:14:00Z", "body": "Hi, our card was charged twice for the Team plan on 1 October. Can you refund the extra charge? Also, we would like to switch to annual billing if it works out cheaper. Thanks, Maria" } ``` ```sh $ curl -sL https://docs.usejinn.com/examples/support-triage.tar.gz | tar -xz $ cd support-triage $ jinn publish function.json → support-triage fnc_a3b59df3be29992f250c2e1e v1 $ jinn run support-triage --prompt "Ticket #4812: charged twice" --in ./in --out ./result run_3b6e0e25251acd12d58a87f3 queued · support-triage v1 run_3b6e0e25251acd12d58a87f3 running agent read {"file_path":"/workspace/in/ticket.json","limit":null,"offset":null} agent read {"file_path":"/workspace/in/policy.md","limit":null,"offset":null} agent lookup_customer {"email":"maria.lopez@brightpath.io"} agent write {"file_path":"/workspace/out/actions.json","content":"[\n {\"charge\": \"ch_3Q8xK22\", \"amount\":… agent write {"file_path":"/workspace/out/reply.md","content":"Hi Maria,\n\nThanks for getting in touch, and so… agent submit_result ✓ succeeded in 17.6s result/actions.json 146 B result/reply.md 910 B ``` `result/actions.json` names the one duplicate charge: ```json [ {"charge": "ch_3Q8xK22", "amount": 348, "reason": "Duplicate charge: second Team plan October charge on 2026-10-01, duplicates ch_3Q8xK21"} ] ``` `result/reply.md` answers both questions, with the prices the tool returned and the signature the policy asks for: ``` Hi Maria, Thanks for getting in touch, and sorry about the double charge. **Duplicate charge:** You're right. Your card was charged twice on 1 October for the Team plan: two charges of $348.00 USD for October. We're refunding the extra charge (ch_3Q8xK22) in full, $348.00 USD. It will go back to the same card within 5 business days. The first charge (ch_3Q8xK21) covers your October subscription as normal. **Switching to annual billing:** Annual billing does work out cheaper for you. With your current 12 seats: - Monthly: $29 per seat per month = $348/month, or $4,176 per year - Annual: $290 per seat per year = $3,480 per year That saves you $696 a year. You can switch at any time in **Settings → Billing → Change plan**. The annual price starts at your next renewal, so October stays on the monthly charge you've already paid. Let me know if you have any other questions. Sam, Acme Support ``` ## Review a pull request A CI job sends the repository at the base commit and the change as a diff. The agent applies the diff, runs the tests and reviews the change. The sample is a small Python module that prices a cart, and a change that adds a bulk discount with four problems in it. ``` pr-review/ function.json in/change.diff in/repo/README.md in/repo/cart/__init__.py in/repo/cart/pricing.py in/repo/tests/__init__.py in/repo/tests/test_pricing.py out/comments.json out/review.md ``` ```json { "name": "pr-review", "system_prompt": "You review a code change. /workspace/in/repo is the repository at the base commit, without .git. /workspace/in/change.diff is the change. Copy the repository to /workspace/repo, apply the change there with git apply, and run the tests (the README says how). Review the change for correctness, tests and clarity; report only problems that matter, most serious first. Write the review to /workspace/out/review.md: a verdict (approve or request changes), then each problem with its file and line. Write line comments to /workspace/out/comments.json as a JSON array of {\"path\": text, \"line\": number, \"body\": text}, with lines in the changed file. Then call submit_result.", "input_manifest": [ { "path": "repo/", "description": "The repository at the base commit" }, { "path": "change.diff", "description": "The change" } ], "output_manifest": [ { "path": "review.md", "description": "The review" }, { "path": "comments.json", "description": "Line comments" } ] } ``` To make the same input from your own repository in CI: `git archive origin/main | tar -x -C in/repo` copies the base commit without `.git`, and `git diff origin/main...HEAD > in/change.diff` writes the change. `jinn run` exits with status 1 when the run fails, so the CI step fails with it. ```sh $ curl -sL https://docs.usejinn.com/examples/pr-review.tar.gz | tar -xz $ cd pr-review $ jinn publish function.json → pr-review fnc_df4a8e0e37c74cfeb2ac9c32 v1 $ jinn run pr-review --prompt "Review the change: a bulk discount for large orders." --in ./in --out ./result run_60feb4f64280e6e41121b756 queued · pr-review v1 run_60feb4f64280e6e41121b756 running agent bash {"command":"cd /workspace/in; cat change.diff; ls -R repo | head -50; cat repo/README*","timeout_ms… agent bash {"command":"cp -r /workspace/in/repo /workspace/repo; cd /workspace/repo; git apply ../in/change.di… agent bash {"command":"mkdir -p /workspace/out; cd /workspace/out\ncat \u003e review.md \u003c\u003c'EOF'\n# R… agent submit_result ✓ succeeded in 26.7s result/comments.json 1.2 KB result/review.md 2.1 KB ``` `result/review.md` found all four problems, ran the tests and worked out the right number: ``` # Review: bulk discount for large orders **Verdict: request changes** I applied the diff and ran `python3 -m unittest`. 4 tests pass and the new `test_bulk` fails (`AssertionError: 2840 != 2860`). Neither number is right: 20 pens at 150¢ is 3000¢, and 5% off that is **2850¢**. ## Problems 1. **`subtotal` changes the caller's items (cart/pricing.py:17).** It writes the discounted price back into `item["price_cents"]`. Each later call to `subtotal`/`total` on the same cart takes another 5% off: 150 → 142, then 135, and so on. The cart's stored unit price is also wrong after the call, so anything that shows or saves it is affected. The discount should be worked out in a local value, and the input should not be touched. 2. **Rounding each unit gives the wrong amount (cart/pricing.py:17).** The code takes 5% off the unit price and rounds it before multiplying by the quantity. Python's `round` rounds halves to the nearest even number, so 142.5 becomes 142, and multiplying by 20 makes the error 20× bigger: 2840 instead of 2850. It also goes through floats, although the module says amounts stay whole numbers. Better to discount the line total with integer maths, the same way `apply_code` does: `line = price * qty; line -= line * BULK_PERCENT // 100`. That gives 2850. 3. **Off by one at the threshold (cart/pricing.py:16).** The docstring (line 13) says "10 or more", but `> BULK_QUANTITY` only applies the discount at 11 or more. A line of exactly 10 gets nothing. It should be `>=`. The docstring also hard-codes 10 and 5%; it should point to the constants instead. 4. **The test expects the wrong value and misses the important cases (tests/test_pricing.py:21).** 2860 is not 5% off 3000, so the test fails against this code, and it would still fail after the fix. It should expect 2850. Tests to add: - quantity exactly 10, where the discount applies, and 9, where it does not; - calling `subtotal` twice on the same items gives the same result and leaves `price_cents` unchanged; - a `total` that has both a bulk line and a discount code, to pin down the order the two discounts are applied in. ``` `result/comments.json` holds the same points as line comments, to post with your Git host's API. ## A weekly report from data A scheduler starts a run every Monday with the last two weeks' orders as CSV files. The agent compares the weeks and returns a report with one chart per finding. The sample has 126 rows: orders, revenue and refunds by day, region and plan. ``` monday-report/ function.json in/last_week.csv in/this_week.csv out/charts/apac_pro_growth.png out/charts/eu_starter_orders_drop.png out/charts/us_team_refunds_spike.png out/report.md ``` ```json { "name": "monday-report", "setup": ["apt-get update", "apt-get install -y --no-install-recommends python3-pandas python3-matplotlib"], "system_prompt": "/workspace/in holds two weeks of orders: last_week.csv and this_week.csv, one row per day, region and plan. Compare the weeks with Python. Find the three changes that matter most to the business, with numbers. Write /workspace/out/report.md: one short paragraph per change, biggest first, each pointing to its chart. Save one PNG chart per change in /workspace/out/charts/, named after the change. Then call submit_result.", "input_manifest": [ { "path": "last_week.csv", "description": "Last week's orders" }, { "path": "this_week.csv", "description": "This week's orders" } ], "output_manifest": [ { "path": "report.md", "description": "The report" }, { "path": "charts/", "description": "One PNG per change" } ] } ``` The setup installs pandas and matplotlib before the agent starts; it counts towards the run's time. ```sh $ curl -sL https://docs.usejinn.com/examples/monday-report.tar.gz | tar -xz $ cd monday-report $ jinn publish function.json → monday-report fnc_46fe4135e812538cc27cff0d v1 $ jinn run monday-report --prompt "Week 40 against week 39." --in ./in --out ./result run_b210108893d007a9c9926646 queued · monday-report v1 run_b210108893d007a9c9926646 running setup apt-get update → 0 setup apt-get install -y --no-install-recommends python3-pandas python3-matplotlib → 0 agent bash {"command":"cd in; head -5 *.csv; wc -l *.csv; python3 -c \"import pandas,matplotlib;print('ok')\""… agent bash {"command":"cd in; python3 -c \"\nimport pandas as pd\na=pd.read_csv('last_week.csv');b=pd.read_csv… agent bash {"command":"cd in; python3 -c \"\nimport pandas as pd\nd=pd.concat([pd.read_csv('last_week.csv'),pd… agent bash {"command":"mkdir -p out/charts; cat \u003e /workspace/an.py \u003c\u003c'EOF'\nimport pandas as pd… agent read {"file_path":"/workspace/out/charts/eu_starter_orders_drop.png","limit":null,"offset":null} agent read {"file_path":"/workspace/out/charts/us_team_refunds_spike.png","limit":null,"offset":null} agent bash {"command":"cat \u003e /workspace/out/report.md \u003c\u003c'EOF'\n# Week 40 (28 Sep – 4 Oct 2026)… agent submit_result ✓ succeeded in 1m29s result/charts/apac_pro_growth.png 43.2 KB result/charts/eu_starter_orders_drop.png 54.6 KB result/charts/us_team_refunds_spike.png 33.2 KB result/report.md 2.5 KB ``` `result/report.md` leads with the change that costs most and explains each with numbers: ``` # Week 40 (28 Sep – 4 Oct 2026) compared with week 39 (21–27 Sep) Overall, orders fell from 1,055 to 942 (−10.7%), but revenue only slipped from $56,384 to $55,799 (−1.0%). That small revenue dip comes from a large loss offset by a large gain, and a refund problem sits underneath it. Prices did not change in either week (Starter $19, Pro $79, Team $290). The `revenue_usd` column always equals orders × price, so it is gross revenue and does not subtract refunds. ## 1. EU Starter orders dropped by half from Wednesday, and they haven't recovered EU Starter fell from 270 to 138 orders (−49%), which cost $2,508 of revenue ($5,130 → $2,622). Nearly all of the week's net order decline comes from this one segment (−132 of −113 orders overall). Monday and Tuesday were normal at 35 orders a day. On Wednesday 30 Sep it fell to about 14 a day and stayed there through Sunday, against roughly 38.6 a day in week 39. No other region or plan shows this pattern. An overnight step this sharp looks more like a technical or pricing/checkout problem in EU (for example payments, a localisation release, or a campaign that ended) than a change in demand. If the new level holds, it costs about $3,300 a week. See `charts/eu_starter_orders_drop.png`. ## 2. US Team refunds rose sixfold, to a third of orders US Team orders were flat (34 → 36), but refunds went from 2 to 12. The refund rate rose from 6% to 33%. Refunds were spread across all seven days, so this is not a one-day blip. The data counts refunds but doesn't give their amounts. If each one is a full $290 refund, that is about $3,480 refunded this week against $580 last week: roughly $2,900 of extra losses that don't appear in the revenue column. This is the highest-value plan, so a refund spike here points to a product, onboarding or billing issue with large accounts and is worth investigating now. Excluding this segment, refunds across the rest of the business actually fell from 20 to 15. See `charts/us_team_refunds_spike.png`. ## 3. APAC Pro grew 49%, the biggest gain this week APAC Pro orders rose from 43 to 64 (+49%), adding $1,659 of revenue ($3,397 → $5,056). This is the only large positive change, and it offsets about two-thirds of the EU Starter loss. The gain held all week, at 8–10 orders a day against 5–7 the week before, so it looks like a real increase rather than a one-off. The cause should be identified (for example a campaign, a partner or a price test) and repeated if possible. See `charts/apac_pro_growth.png`. ``` ![EU Starter daily orders: about 38 a day in week 39, then from Wednesday of week 40 about 14 a day](/examples/monday-report/out/charts/eu_starter_orders_drop.png) ![US Team refunds per day: 2 in week 39, 12 in week 40](/examples/monday-report/out/charts/us_team_refunds_spike.png) ![APAC Pro daily orders: 5 to 7 a day in week 39, 8 to 10 in week 40](/examples/monday-report/out/charts/apac_pro_growth.png) To run it every Monday, put the export and the run in a script for cron: ```sh $ cat weekly.sh #!/bin/sh set -e export JINN_KEY=… # a run key for monday-report ./export-csv.sh ./in # writes last_week.csv and this_week.csv jinn run monday-report --prompt "Week $(date +%V)" --in ./in --out "./reports/$(date +%F)" $ crontab -l 0 9 * * 1 /opt/reports/weekly.sh ``` ## Check a web page on a phone The agent opens a website's page in headless Chromium at a phone's width, looks at the screenshots, follows the links and reads the HTML and CSS. It returns what a visitor would see go wrong, the cause and the fix, with a screenshot of each problem. The sample is a small café's site with some common mistakes. ``` page-check/ function.json in/site/beans.svg in/site/index.html in/site/menu.html in/site/style.css in/site/visit.html out/findings.md out/shots/index-phone-full.png out/shots/index-phone.png out/shots/order-404.png ``` ```json { "name": "page-check", "system_prompt": "/workspace/in/site is a website. Check the page the prompt names as a visitor on a phone would see it. Take phone-size screenshots with headless Chromium, for example: chromium --headless --no-sandbox --hide-scrollbars --window-size=390,844 --screenshot=/workspace/out/shots/page.png file:///workspace/in/site/index.html. Look at each screenshot with the read tool. Check that every link works, that nothing is wider than the screen, that text is readable, and that images have alt text. Read the HTML and CSS to find each cause. Write /workspace/out/findings.md: each problem, most serious first, with what a visitor sees, the cause, and the fix. Keep a screenshot of each visible problem in /workspace/out/shots/ as PNG. Then call submit_result.", "input_manifest": [ { "path": "site/", "description": "The website's files" } ], "output_manifest": [ { "path": "findings.md", "description": "What is wrong, and how to fix it" }, { "path": "shots/", "description": "Screenshots of problems" } ] } ``` ```sh $ curl -sL https://docs.usejinn.com/examples/page-check.tar.gz | tar -xz $ cd page-check $ jinn publish function.json → page-check fnc_b8bf4b627bba192840ece5b7 v1 $ jinn run page-check --prompt "Check index.html." --in ./in --out ./result run_8efe0c15a3315372f3d4f90e queued · page-check v1 run_8efe0c15a3315372f3d4f90e running agent bash {"command":"cd /workspace/in/site; find . -type f | head -50; echo; cat index.html; echo; cat *.css… agent bash {"command":"cd /workspace/in/site; mkdir -p /workspace/out/shots; cat beans.svg | head -5; ls order… agent read {"file_path":"/workspace/out/shots/index-phone.png","limit":null,"offset":null} agent read {"file_path":"/workspace/out/shots/order-404.png","limit":null,"offset":null} agent bash {"command":"cd /workspace/in/site; chromium --headless --no-sandbox --hide-scrollbars --window-size… agent read {"file_path":"/workspace/out/shots/index-phone-full.png","limit":null,"offset":null} agent bash {"command":"cat \u003e /workspace/out/findings.md \u003c\u003c'EOF'\n# index.html: phone check (390… agent submit_result ✓ succeeded in 50.7s result/findings.md 5.5 KB result/shots/index-phone-full.png 62.3 KB result/shots/index-phone.png 47.9 KB result/shots/order-404.png 13.6 KB ``` ![The page at 390 pixels wide: the headline and the Order beans button are cut off at the right edge](/examples/page-check/out/shots/index-phone.png) ![The whole page: the roasts table runs off the screen, and the opening hours are pale grey on cream](/examples/page-check/out/shots/index-phone-full.png) ![Order beans opens a file-not-found error](/examples/page-check/out/shots/order-404.png) `result/findings.md` lists seven problems, most serious first: - "Order online" and "Order beans" lead to a missing page (broken links) - The hero section is wider than the screen, so the headline and button are cut off - The "This week's roasts" table is 820 px wide, so prices are off-screen - No viewport meta tag - Opening hours and footer text are too faint and too small to read - The hero image has no alt text - Minor: the header nav wraps, and the footer link is browser-default blue The first, as the run wrote it: ``` ## 1. "Order online" and "Order beans" lead to a missing page (broken links) - **What a visitor sees:** Tapping "Order online" in the header or the orange "Order beans" button opens an error page ("Your file couldn't be accessed", ERR_FILE_NOT_FOUND). The visitor cannot buy anything, which is the main thing the page asks them to do. See `shots/order-404.png`. - **Cause:** Both `` links point to `order.html`, but that file is not in the site. The site only has index.html, menu.html, visit.html, style.css and beans.svg. - **Fix:** Add `order.html`, or point both links at the real ordering page (for example the shop's external store URL). ``` To check a site that is online instead, take out the input manifest, leave out `--in`, and put the address in the prompt. --- # Functions A function is a named definition. Publishing it again makes a new **version**. A version never changes. A run uses the version you name, or the latest. A function's id (`fnc_…`) is its address. Two functions in an account can have the same name; the CLI accepts a name only when exactly one function has it. ## Definition This is the [support ticket example](/examples#answer-a-support-ticket)'s function: ```json { "name": "support-triage", "system_prompt": "You answer billing tickets for Acme. /workspace/in holds the ticket (ticket.json) and the billing policy (policy.md). Look the customer up by the ticket's email with lookup_customer, and follow the policy exactly. Write the reply to the customer to /workspace/out/reply.md. Write the refunds to make to /workspace/out/actions.json as a JSON array of {\"charge\": id, \"amount\": number, \"reason\": text}; write [] if there are none. Then call submit_result.", "custom_tools": [{ "name": "lookup_customer", "description": "Find a customer by email: their plan, seats, prices and recent charges.", "parameters": { "type": "object", "properties": { "email": { "type": "string" } }, "required": ["email"], "additionalProperties": false }, "url": "https://docs.usejinn.com/tools/lookup-customer", "secret": "sk_example_support" }], "input_manifest": [ { "path": "ticket.json", "description": "The ticket" }, { "path": "policy.md", "description": "The billing policy" } ], "output_manifest": [ { "path": "reply.md", "description": "The reply to send" }, { "path": "actions.json", "description": "Refunds to make" } ] } ``` Besides `name`, `system_prompt` and `output_manifest` are required. A function may take no input, but it always returns at least one file or folder. Publishing fills in each field you leave out, and the version stores what it filled in. Set a field to change it. | Field | Value | Left out | |---|---|---| | `name` | The function's name, read by `jinn publish`. The API takes it beside the definition, in `POST /v1/functions`. | Required | | `system_prompt` | The agent's instructions. The run's prompt is the agent's first message. | Required | | `base` | The disk the VM boots. `jinn bases` lists them. See [Environment](/environment#the-base). | The newest base | | `setup` | Commands run before the agent starts. See [Bases and setup](#bases-and-setup). | None | | `provider` | The provider by name or id, and its version. `openai` or `openai@latest` uses the newest version when the run starts; `openai@3` uses version 3. `prv_…@3` and `prv_…@latest` work too. A name is stored as its provider's id; publishing refuses a name no provider has, or one two providers share. | The account's only provider, at `@latest`. With none or several, publishing refuses and asks for one. | | `tools` | Any of `bash`, `read`, `write`, `edit`, `screenshot`, `web_search`. `[]` is none. The agent always has `submit_result`. | All of them | | `custom_tools` | Tools you serve over HTTPS. See [Custom tools](#custom-tools). | None | | `input_manifest` | Files and folders the input must hold. Checked before the agent starts. | None | | `output_manifest` | Files and folders `/workspace/out` must hold: at least one. The agent cannot finish without them. | Required | | `environment` | Variables for setup and the agent's commands. See [Secrets](#secrets). | None | | `size` | `s`, `m`, `l` or `xl`. See [Environment](/environment#sizes). | `m` | | `timeout_minutes` | 1 to 1440. Setup counts towards it. | 30 | A version keeps the base it was published with. To move to a newer base, publish again. ## Manifests Each entry has a `path` and a `description`, and may have a `max_bytes`. Left out, `max_bytes` is the limit: 5 GB for an input file, 50 GiB for the output. A path ending in `/` is a folder: it must exist, may be empty, and `max_bytes` limits everything inside it. Jinn checks existence and size only. What a file holds is between you and your system prompt. - Input: a missing or oversized entry fails the run with `input` before the agent starts. - Output: when the agent calls `submit_result`, Jinn checks `/workspace/out`. If something is missing or too large, the agent gets the list of problems and can fix them. Files not in the manifest are returned too. ## Bases and setup Setup works like a Dockerfile's `RUN` lines. Each command runs as root, in its own `bash -euo pipefail -c`, with network access and the function's environment. A command that exits with anything but 0 fails the run with `setup`, and the detail shows its output. ```json "setup": [ "apt-get update", "apt-get install -y --no-install-recommends python3-pip poppler-utils", "pip install --break-system-packages pandas==2.2.3" ] ``` Debian packages come from `deb.debian.org`. `apt-get update` takes about 3 seconds. To bring your own code, put it in the input folder and install it from `/workspace/in`. ## Providers A provider is a model, its settings and your vendor key, for OpenAI, Anthropic or xAI. Runs call the model with your key; the vendor bills you for tokens. Publishing checks the key and the model against the vendor's model list. No response returns the key. | Field | Value | |---|---| | `model.provider` | `openai`, `anthropic` or `xai` | | `model.model` | A model id from `jinn models ` | | `model.reasoning_effort` | One of the efforts the model lists, or empty | | `model.compact_at_tokens` | When the conversation reaches this size, the run compacts it. At least 8000; at least 50000 for Anthropic. | | `model.max_output_tokens` | The most tokens per reply. 0 uses the model's maximum. | A new key or setting is a new provider version. Functions that use `@latest` get it at their next run. Make providers with `jinn provider`, in the console, or with `POST /v1/providers`. ## Custom tools A custom tool is an endpoint you serve. When the agent calls it, Jinn sends the arguments as JSON in a `POST` to `url`. The response body, as text, is the result. This is the tool from the [support ticket example](/examples#answer-a-support-ticket); its endpoint is real, so the example runs as it is: ```json { "name": "lookup_customer", "description": "Find a customer by email: their plan, seats, prices and recent charges.", "parameters": { "type": "object", "properties": { "email": { "type": "string" } }, "required": ["email"], "additionalProperties": false }, "url": "https://docs.usejinn.com/tools/lookup-customer", "secret": "sk_example_support" } ``` Each call carries two headers: | Header | Value | |---|---| | `Authorization` | `Bearer `. Refuse a call without it. | | `Jinn-Run` | The run the call is for (`run_…`). Use it to log or trace the call, or to look the run up. | - `parameters` is a JSON Schema object. An object in it sets `additionalProperties` to `false`. - An error status, a failed request or no answer within `timeout_seconds` (1 to 600; 30 when left out) is returned to the agent as an error. The run continues. - Jinn calls public HTTPS addresses only and does not follow redirects. - The call comes from Jinn, not from the VM. The secret never enters the VM. ## Web search `web_search` searches the public web through the function's provider, on your key. The vendor bills the searches. | Vendor | How the agent searches | |---|---| | OpenAI | Each search is a separate call to `gpt-5.6` with OpenAI's web search tool. The agent gets the findings and their source URLs. | | Anthropic | Anthropic's web search tool, up to 20 searches per model reply. An organization's admin can turn web search off in the Claude Console. Then every model call with the tool is refused, and the run fails with `provider`. For such an organization, leave `web_search` out of `tools`. | | xAI | xAI's web search tool. | A run's `usage.model.web_searches` counts its searches. ## Secrets Send a secret value once, as `secret`, in a tool or an environment variable. Jinn encrypts it for your account and the version stores only a `secret_id`. To keep the value in a later version, send that `secret_id` instead of `secret`. - A tool's secret is sent only to a tool at the same URL. - A secret variable is in the VM's environment, so the agent can read it. - Variable names cannot start with `JINN_`. ## Publish ```sh $ jinn publish function.json → support-triage fnc_5d2a91c07e4b38f6a1d0c2e9 v14 ``` `jinn publish` makes the function if no function has the file's `name`, and otherwise publishes its next version. With the API, `POST /v1/functions` makes a function and `POST /v1/functions/{id}/versions` publishes the next version. --- # Runs A run is one call of a function. Every start makes a new run with a new id (`run_…`). A run cannot be cancelled or continued; start another. ## Start a run ```sh $ jinn run support-triage --prompt "Ticket #4812: charged twice" --in ./in --out ./result ``` With the API, upload the input folder as one gzip-compressed tar (`tar -czf in.tar.gz -C in .`), then start the run: ```http POST /v1/files {"bytes": 18432, "sha256": "9f2c41…"} ← 201 {"id": "file_3b8e0c5a9d2f41e7b6c08a13", "url": "https://…", "method": "PUT", "headers": {…}} PUT with the returned headers and the .tar.gz as the body POST /v1/functions/fnc_5d2a91c07e4b38f6a1d0c2e9/runs {"prompt": "Ticket #4812: charged twice", "input": "file_3b8e0c5a9d2f41e7b6c08a13"} ← 201 {"id": "run_7c41e0a9d2b35f8e61c09a4d", "state": "queued", …} ``` | Field | Value | |---|---| | `prompt` | Required. The agent's first message, word for word. | | `input` | An uploaded `.tar.gz` (`file_…`), unpacked into `/workspace/in`. Required if the input manifest names files. | | `version` | A version number. Leave it out for the latest. | | `webhook` | An HTTPS address for the result. See [Webhooks](/webhooks). | | `external_reference` | Your own id for the run, up to 256 bytes. Returned as sent. | The input may hold regular files and folders only; links are refused. Unpacked, it must fit the run's `/workspace` disk (see [sizes](/environment#sizes)). A plain `.tar` is refused. ## States `queued` → `running` → `succeeded` or `failed`. A run starts within seconds when a machine has room. When Jinn has to start a machine, the run waits about 90 seconds more. A run that has not started after 15 minutes fails with `infrastructure`. A `running` run goes through three steps, in its log: the VM boots (about 10 seconds), setup runs, then the agent works until it calls `submit_result`. ## Results A succeeded run has `output`: the files in `/workspace/out` with their sizes, and `url`, a link to the folder as one `.tar.gz`, with its `bytes` and `sha256`. Each read of the run makes a new link, valid for 15 minutes. Jinn compresses at gzip's fastest level: text shrinks several times, media barely. ```sh $ jinn output run_7c41e0a9d2b35f8e61c09a4d --out ./result ``` A failed run has `failure` and `detail`: | Failure | Cause | |---|---| | `input` | The input is not a readable `.tar.gz`, holds a link, unpacks to more than `/workspace` holds, or does not match the input manifest. | | `setup` | A setup command exited with a status other than 0. The detail shows its output. | | `no_submission` | The agent stopped without delivering the output manifest. | | `timeout` | The run worked past `timeout_minutes`. | | `provider` | The model vendor refused a call, for example for a wrong key or no quota. The detail quotes the vendor. | | `infrastructure` | Jinn could not start or finish the run. Start it again. | | `suspended` | The account was suspended while the run worked. | | `credit` | The account's balance went more than $10 below its minimum while the run worked. Add credit to start runs again. | `usage` records the VM's size and seconds, and the model's calls and tokens. ## Logs The log records each step: setup commands with their output and exit status, the model's messages, and each tool call with its result. A running run adds to it every 30 seconds. ```sh $ jinn logs run_7c41e0a9d2b35f8e61c09a4d --follow ``` `GET /v1/runs/{id}/log` returns links to the log's parts, in order. Each part is JSON lines: `{"at": "2026-10-06T09:14:03Z", "kind": "tool_call", "name": "bash", "arguments": "…"}`. ## Credit - An account can start as many runs as it likes. When every machine is busy, runs wait for one; a run not started within 15 minutes fails with `infrastructure`. - A run is billed per started minute of compute, from when a machine takes it until it ends. See [Pricing](/pricing). - A run starts only while the account has credit. A start without credit gets `402`. A run that has started keeps running until the balance is more than $10 below the account's minimum; then it stops within 5 minutes, fails with `credit`, and is billed for the minutes it used. - A suspended account's starts get `403`, and its running runs stop within 5 minutes. --- # Environment Each run gets a new Firecracker VM. Nothing is shared with other runs, and nothing is kept after the run. ## Sizes | Size | vCPUs | Memory | Disk for `/workspace` | |---|---|---|---| | `s` | 1 | 2 GiB | 16 GiB | | `m` | 2 | 4 GiB | 32 GiB | | `l` | 4 | 8 GiB | 64 GiB | | `xl` | 8 | 16 GiB | 128 GiB | A VM sees its size's vCPUs. Each vCPU is a hardware thread, and it is not dedicated: VMs on a machine share its threads in proportion to vCPUs. A machine has 8 threads on 4 cores and runs VMs with at most 14 vCPUs in all. Each VM's disk is limited to 50 MB/s and 5,000 operations a second per vCPU, and its network to 25 MB/s per vCPU. ## The base The base is the VM's root disk. Bases never change; a new base has a new name. `jinn bases` lists them. `debian-12-20260801-0f9c2251`, the newest, is Debian 12 with: Python 3, git, curl, jq, ripgrep, xz, zstd, Chromium, OpenSSH client, and a 1440×900 Wayland display (sway, with Xwayland for X11 programs). The `screenshot` tool captures the display. From `bash`, `ydotool` types, clicks, drags and scrolls, `swaymsg` lists and moves windows, and `grim` and `wl-copy`/`wl-paste` take screenshots and use the clipboard. The older `debian-12-20260801-72c15f01` is the same with a plain background. Install anything else with [setup](/functions#bases-and-setup). ## Files | Path | What it holds | |---|---| | `/workspace/in` | The input `.tar.gz`, unpacked. | | `/workspace/out` | What the run returns. Everything here is delivered. | | `/workspace` | The run's own disk, sized by the function's size. | Setup and the agent's commands run as root. ## Network The VM reaches the internet. It cannot reach: - private addresses (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `100.64.0.0/10`); - link-local addresses, including cloud metadata services (`169.254.0.0/16`); - other VMs, or the machine it runs on; - mail servers on port 25. New connections are limited to 200 a second. ## Credentials - Your model key never enters the VM. The model is called from outside it. - Custom tools are called from outside the VM. Their secrets never enter it. - Secret environment variables do enter the VM, as variables. - The VM has no cloud credentials. ## What is kept The VM and its disks are destroyed when the run ends. Inputs, outputs and logs are stored encrypted with a Jinn KMS key, and deleted 30 days after the run, with the run's record. Functions, their versions and their secrets are kept while the account exists. --- # Limits | What | Limit | |---|---| | Prompt | 128 KiB | | System prompt | 64 KiB | | Setup | 64 commands, 16 KiB each | | Environment | 64 variables; a value up to 64 KiB, a secret up to 4 KiB | | Custom tools | 16 per function; timeout 1 to 600 seconds | | Manifests | 64 entries each; a description up to 2,000 bytes | | Input `.tar.gz` | 5 GB uploaded; unpacked, no more than the size's `/workspace` disk | | Output | 50 GiB in `/workspace/out` | | Files listed in a run's `output.files` | 1,000; `output.file_count` counts all | | `timeout_minutes` | 1 to 1,440 | | Start | A run not started within 15 minutes fails | | `external_reference` | 256 bytes | | Function and provider names | 1 to 40 characters: `a-z`, `0-9` and `-`, starting with a letter or digit | | Output link | 15 minutes from the read that made it | | Webhook | 3 attempts: at once, after 1 minute, after 10 minutes | | Retention | Runs, inputs, outputs and logs: 30 days | | Invite | 7 days | | CLI sign-in | 10 minutes to approve | | Credit purchase | $5 to $500 | | Overdraft | Running runs stop when the balance is more than $10 below the account's minimum | | Compute | $0.0025 per GiB a minute ($0.15 per GiB-hour), per started minute. See [Pricing](/pricing). | --- # Pricing Jinn charges for compute by the minute: $0.0025 per GiB of memory a minute ($0.15 an hour), for each started minute. Your model provider bills the tokens, on your own key. ## Sizes | Size | vCPUs | Memory | A minute | An hour | |---|---|---|---|---| | `s` | 1 | 2 GiB | $0.005 | $0.30 | | `m` | 2 | 4 GiB | $0.01 | $0.60 | | `l` | 4 | 8 GiB | $0.02 | $1.20 | | `xl` | 8 | 16 GiB | $0.04 | $2.40 | ## What counts - A run's minutes start when a machine takes it and end when its result is written. Setup counts. Time in the queue does not. - Each started minute is billed. A run of 4 minutes 10 seconds is billed 5 minutes. - A run is billed whether it succeeds or fails. - Inputs, outputs, logs and functions are free within the [limits](/limits). A 7-minute review on size `m` costs 7 minutes × $0.01 = $0.07. ## Credit - Runs are paid from the account's prepaid credit. An owner adds $5 to $500 at a time under **Account** in the console. Stripe sells the credit as merchant of record, and adds any sales tax, VAT or GST at checkout. - A run starts only while the account has credit. A run that has started keeps running until the balance is more than $10 below the account's minimum; then it stops within 5 minutes, fails with `credit`, and is billed for the minutes it used. - The console's **Usage** lists each run's minutes, memory and charge. `GET /v1/usage` returns the same. - `GET /v1/account` returns the price as `compute_rate_usd_nanos_per_gib_hour` (150000000 is $0.15). --- # API Base URL: `https://api.usejinn.com/v1`. Requests and responses are JSON. [openapi.json](/openapi.json) describes every route and field. ## Authentication Send an account key as a bearer token: ```sh $ curl https://api.usejinn.com/v1/functions -H "Authorization: Bearer $JINN_KEY" ``` An owner makes keys in the console under **Account**. `jinn login` makes one for the CLI. A key belongs to one account. Its token is shown once. A revoked key stops working at once. | Scope | Can | |---|---| | `admin` | Read and publish functions and providers, upload files, start and read every run in the account. | | `run` | Upload files, start the functions it names, and read their runs. A run key for `["*"]` runs every function in the account, including ones made after it. | No key can manage members, invites, keys or credit; those need a person signed in to the console. ## Routes | Route | Does | |---|---| | `POST /files` | Starts an upload. Send `bytes` and `sha256`; then `PUT` the file to the returned `url` with the returned `headers`. Returns the `file_…` id. | | `GET /bases` | Lists the bases. | | `GET /functions` | Lists functions, each with its latest definition and last run. | | `POST /functions` | Makes a function and its first version: `name` and a definition. | | `GET /functions/{id}` | Returns a function and its versions. | | `POST /functions/{id}/versions` | Publishes the next version. | | `POST /functions/{id}/runs` | Starts a run. See [Runs](/runs#start-a-run). | | `GET /runs` | Lists runs, newest first, 50 a page. Filters: `function`, `state` (`active`, `succeeded`, `failed`). Pass the response's `next` as `before` for the next page. A `run` key must give `function`. | | `GET /runs/{id}` | Returns a run. | | `GET /runs/{id}/log` | Returns links to the run's log parts. | | `GET /providers` | Lists providers and their versions. | | `POST /providers` | Makes a provider: `name`, `model` and `key`. | | `POST /providers/{id}/versions` | Publishes a provider's next version: `model` and `key`. | | `POST /catalog` | Lists the models a vendor key can use: `provider` and `key`. The key is not stored. | | `GET /webhooks/public-key` | Returns the account's webhook public key. | ## Errors A refused request has an HTTP status and a body with a stable `code` and a message for people: ```json {"code": "no_credit", "error": "this account has no credit left; an owner can add credit in the console"} ``` Switch on `code`; the message may change. Retry `conflict` and `internal`. The others need a change first. | Status | `code` | Meaning | |---|---|---| | `400` | `invalid_request` | The request is not valid; the message names the field. | | `400` | `invalid_signature` | A Stripe webhook's signature did not verify. | | `400` | `account_required` | A person's token needs the Jinn-Account header. | | `401` | `unauthenticated` | No key or token, or it is wrong, expired or revoked. | | `402` | `no_credit` | The account's balance is at its minimum; runs cannot start. | | `403` | `key_scope` | The key's scope does not allow this route (a run key). | | `403` | `people_only` | The route is for people signed in to the console, not keys. | | `403` | `not_a_member` | The person is not a member of the named account. | | `403` | `owners_only` | The route is for account owners. | | `403` | `suspended` | The account is suspended. | | `404` | `not_found` | No such route, or no such resource in this account. | | `405` | `method_not_allowed` | The route does not take this method. | | `409` | `conflict` | Another write changed the same thing first. Retry. | | `500` | `internal` | Jinn failed. Retry; if it persists, write to support@usejinn.com. | | `502` | `vendor_unavailable` | The model vendor's catalog could not be read. | | `503` | `purchases_closed` | Buying credit is not open. | --- # Webhooks A run started with `webhook` posts its result there when it ends. The body is one event: ```json { "id": "evt_7c41e0a9d2b35f8e61c09a4d", "type": "run.succeeded", "created": "2026-10-06T09:14:03Z", "data": {"id": "run_7c41e0a9d2b35f8e61c09a4d", "state": "succeeded", "output": {"url": "https://…", "files": […]}, …} } ``` `type` is `run.succeeded` or `run.failed`. `data` is the run as `GET /v1/runs/{id}` returns it. ## Delivery - Answer with a `2xx` status within 10 seconds. - Jinn tries three times: at once, after 1 minute and after 10 minutes. Then it stops. Read the run if you missed it. - Jinn posts only to public HTTPS addresses and does not follow redirects. - Every attempt carries the same `webhook-id`. Use it to ignore repeats. ## Verify the signature Webhooks follow [Standard Webhooks](https://www.standardwebhooks.com/) with Ed25519 signatures (`v1a`). Each account has its own key. Its public key (`whpk_…`) is in the console under **Account**, and at `GET /v1/webhooks/public-key`. | Header | Value | |---|---| | `webhook-id` | The event id. | | `webhook-timestamp` | When it was signed, in Unix seconds. | | `webhook-signature` | `v1a,` and the base64 Ed25519 signature of `{webhook-id}.{webhook-timestamp}.{body}`. | Refuse the request if the signature does not verify, or if the timestamp is more than 5 minutes from now. Go: ```go event, err := jinn.VerifyWebhook(publicKey, r.Header, body, time.Now()) ``` TypeScript: ```ts const event = await verifyWebhook(publicKey, request.headers, await request.text()); ``` Python, with `cryptography`: ```python import base64 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey key = Ed25519PublicKey.from_public_bytes(base64.b64decode(public_key.removeprefix("whpk_"))) signed = f"{headers['webhook-id']}.{headers['webhook-timestamp']}.{body}".encode() signature = base64.b64decode(headers["webhook-signature"].split(",", 1)[1]) key.verify(signature, signed) # raises InvalidSignature ``` --- # CLI ```sh jinn login jinn bases jinn functions jinn publish FILE jinn run FUNCTION --prompt TEXT [--in DIR] [--out DIR] [--version N] [--webhook URL] [--ref REF] [--detach] jinn runs [--function FUNCTION] [--state active|succeeded|failed] jinn show RUN jinn logs RUN [--follow] jinn output RUN --out DIR jinn providers jinn models VENDOR < vendor key jinn provider NAME --vendor V --model M [--effort E] [--compact N] [--max-output N] < vendor key jinn version jinn COMMAND … --json ``` `FUNCTION` is a function's id, or its name when only one function has it. Run `jinn help` for the same list. Add `--json` to any command to get JSON: one document, or JSON lines for a run or a log as it happens. The exit status is 0 when it worked, 1 when the run failed, 2 for a wrong command, 3 when the API refused (the error's `code` says why) and 4 for anything else. [Use Jinn from an agent](/agents#use-the-cli) shows the JSON. ## Install ```sh $ curl -fsSL https://github.com/usejinn/jinn-cli/releases/latest/download/install.sh | sh ``` The script comes from the CLI's latest [GitHub release](https://github.com/usejinn/jinn-cli/releases). It downloads the build for your system from the same release, checks it against the release's `SHA256SUMS`, and puts `jinn` in `/usr/local/bin`, or `~/.local/bin` if that is not writable. Builds are for Linux and macOS (x86-64 and ARM64) and Windows (x86-64). With Go 1.25 or newer: `go install usejinn.com/jinn@latest`. ## Verify Every release is built by the [release workflow](https://github.com/usejinn/jinn-cli/blob/main/.github/workflows/release.yml) in the public repository, and nowhere else. Releases are immutable: once published, no file in a release and no release tag can change. Each file has a build provenance attestation. To check that a file was built by that workflow from that repository: ```sh $ gh attestation verify jinn-linux-amd64 --repo usejinn/jinn-cli ``` The build is reproducible. To rebuild a release and compare it: ```sh $ git clone https://github.com/usejinn/jinn-cli && cd jinn-cli && git checkout v0.4.0 $ GOTOOLCHAIN=go1.25.12 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w -buildid=" -o jinn-linux-amd64 . $ sha256sum jinn-linux-amd64 # the same as in the release's SHA256SUMS ``` ## Authentication `jinn login` prints a link and a code. Open the link, check that the console shows the same code, choose the account, and approve. The CLI gets an `admin` key named after the machine and saves it in `~/.config/jinn/key`. Revoke it in the console like any key. | Variable | Effect | |---|---| | `JINN_KEY` | Use this key instead of the saved one. Use it on servers and in CI. | | `JINN_API` | Use another API address. | | `NO_COLOR` | Print without colour. Output is also plain when it is not a terminal. | ## Run ```sh $ jinn run pr-review --prompt "Review the change: a bulk discount for large orders." --in ./in --out ./result run_60feb4f64280e6e41121b756 queued · pr-review v1 run_60feb4f64280e6e41121b756 running agent bash {"command":"cd /workspace/in; cat change.diff; ls -R repo | head -50; cat repo/README*","timeout_ms… agent bash {"command":"cp -r /workspace/in/repo /workspace/repo; cd /workspace/repo; git apply ../in/change.di… agent bash {"command":"mkdir -p /workspace/out; cd /workspace/out\ncat \u003e review.md \u003c\u003c'EOF'\n# R… agent submit_result ✓ succeeded in 26.7s result/comments.json 1.2 KB result/review.md 2.1 KB ``` `--in` uploads a folder as the run's input. `--out` downloads the output folder when the run succeeds. Without `--detach`, the CLI follows the run and exits with status 1 if it fails. `--detach` prints the run's id and exits. ## Runs ```sh $ jinn runs --function support-triage run_fc8b8312336d7d3d0c6cace0 support-triage v4 succeeded 2026-10-07 13:03 14.3s run_3b6e0e25251acd12d58a87f3 support-triage v4 succeeded 2026-10-07 12:58 17.6s run_6b8226869e0b7d5493f2a29b support-triage v2 succeeded 2026-10-07 11:09 20s run_53bba625eea3b49a53fa5b87 support-triage v1 succeeded 2026-10-07 10:45 16s ``` `jinn show RUN` prints the whole run as JSON. `jinn logs RUN` prints its log, one JSON event a line; `--follow` prints the agent's steps until the run ends. ## Providers The CLI reads vendor keys from stdin, never from a flag, so keys stay out of your shell history. ```sh $ echo "$OPENAI_API_KEY" | jinn models openai gpt-5.2 low,medium,high (default medium) $ echo "$OPENAI_API_KEY" | jinn provider openai --vendor openai --model gpt-5.2 --effort medium → openai prv_8c1f04d2a7b93e650f1a2c47 v1 · use prv_8c1f04d2a7b93e650f1a2c47@latest in a function's provider ``` Run `jinn provider` again with the same name to publish a new version, for a new key or model. `--compact` defaults to 200000 tokens. --- # SDKs Both SDKs cover the API for keys: functions, providers, runs, uploads, logs, outputs and webhook checks. Neither has dependencies. | Language | Install | Source | |---|---|---| | Go 1.25+ | `go get usejinn.com/go` | [usejinn/jinn-go](https://github.com/usejinn/jinn-go) | | TypeScript: Node 20+, Deno, Bun, browsers | `npm install github:usejinn/jinn-node`, imported as `@usejinn/sdk` | [usejinn/jinn-node](https://github.com/usejinn/jinn-node) | ## Go ```go c := jinn.New(os.Getenv("JINN_KEY")) // The support ticket example: ./in holds ticket.json and policy.md. in, err := c.UploadFolder(ctx, "./in") run, err := c.StartRun(ctx, "fnc_a3b59df3be29992f250c2e1e", jinn.RunRequest{Prompt: "Ticket #4812: charged twice", Input: in}) run, err = c.Wait(ctx, run.ID) if run.State == jinn.Succeeded { err = c.DownloadOutput(ctx, run, "./result") } ``` A refused request returns `*jinn.Error` with `Status` and `Message`. Reference: [pkg.go.dev/usejinn.com/go](https://pkg.go.dev/usejinn.com/go). ## TypeScript ```ts import { Jinn, pack, unpack } from "@usejinn/sdk"; const jinn = new Jinn({ key: process.env.JINN_KEY! }); // The support ticket example's input: the ticket and the billing policy. const input = await jinn.upload(await pack({ "ticket.json": JSON.stringify(ticket), "policy.md": policy })); let run = await jinn.startRun("fnc_a3b59df3be29992f250c2e1e", { prompt: "Ticket #4812: charged twice", input }); run = await jinn.wait(run.id); if (run.state === "succeeded") { const files = await unpack(await jinn.output(run)); console.log(new TextDecoder().decode(files["reply.md"])); } ``` A refused request throws `JinnError` with `status`. --- # Use Jinn from an agent This page is for coding agents and the people who set them up. It says how to read these docs, how to call Jinn without a person watching, and how to tell what went wrong. ## Read the docs - Every page is Markdown at the same address with `.md` added, for example [/runs.md](/runs.md). A request with `Accept: text/markdown` gets the Markdown too. - [llms.txt](/llms.txt) lists every page with one line about it. [llms-full.txt](/llms-full.txt) holds all of them in one file. - [openapi.json](/openapi.json) describes every route, field, error code and limit of the API. Each operation has an `operationId`. ## Authenticate Give the agent an API key in `JINN_KEY`. An owner makes keys in the console under **Account**. Use a `run` key when the agent only starts runs of named functions. Use an `admin` key when it also publishes functions. Keys never expire; revoke one in the console when the agent no longer needs it. The agent never needs a model vendor's key. Providers hold those, and people add them in the console or with `jinn provider`. ## Use the CLI Install it with: ```sh curl -fsSL https://github.com/usejinn/jinn-cli/releases/latest/download/install.sh | sh ``` Add `--json` to any command. Results are printed as one JSON document. A stream (`jinn run` without `--detach`, `jinn logs`) is printed as JSON lines. The CLI never prompts when stdin is not a terminal. ```sh $ jinn run support-triage --prompt "Ticket #4812: charged twice" --in ./in --out ./result --json {"run":{"id":"run_fc8b8312336d7d3d0c6cace0",…,"state":"queued",…},"type":"run"} {"run":{"id":"run_fc8b8312336d7d3d0c6cace0",…,"state":"running",…},"type":"run"} {"at":"2026-10-07T13:03:18.206042976Z","kind":"system","text":"You answer billing tickets for Acme. …","type":"log"} {"arguments":"{\"email\":\"maria.lopez@brightpath.io\"}","at":"2026-10-07T13:03:24.738990358Z","id":"toolu_01AradMuJxWMfkM52RpwN8os","kind":"tool_call","name":"lookup_customer","type":"log"} {"run":{"id":"run_fc8b8312336d7d3d0c6cace0",…,"state":"succeeded",…,"output":{…},…},"type":"run"} {"dir":"./result","file_count":2,"files":[{"path":"actions.json","bytes":154},{"path":"reply.md","bytes":859}],"type":"output"} ``` | Line `type` | Holds | |---|---| | `run` | The run, as `GET /v1/runs/{id}` returns it. Printed when it is queued, when it starts and when it ends. | | `log` | One log event: `at`, `kind` and the event's fields. | | `output` | The folder the output was unpacked into, and its files. | Errors are one JSON object on stderr: `{"error": "…", "exit": 3, "status": 402, "code": "no_credit"}`. | Exit status | Meaning | |---|---| | 0 | Done. For `jinn run`, the run succeeded. | | 1 | The run failed. Its last `run` line has `failure` and `detail`. | | 2 | The command was used wrongly. | | 3 | The API refused the request. `code` says why; see [API errors](/api#errors). | | 4 | Anything else: the network, a local file. | To start a run and come back later, use `--detach`, then `jinn show RUN --json` until `state` is `succeeded` or `failed`. ## Use the API The API is plain HTTPS and JSON at `https://api.usejinn.com/v1`. A refused request has a stable `code`: ```json {"code": "no_credit", "error": "this account has no credit left; an owner can add credit in the console"} ``` Switch on `code`, not on the message. Retry `conflict` and `internal`. Do not retry the others without a change. For results, poll `GET /v1/runs/{id}`, or start the run with a `webhook` and wait for the [event](/webhooks). ## Write a function an agent can rely on - Name every file the run must return in `output_manifest`. The agent inside the run cannot finish until they exist, so the caller never gets a half-done result. - End the system prompt with what to write where, then "call submit_result". - Keep `timeout_minutes` close to what the work takes. A run that hangs costs until its timeout. - Give setup commands exact package versions, so every run starts the same. --- # Console The console is at [app.usejinn.com](https://app.usejinn.com). Sign in with your email address; Jinn emails you a code. ## Accounts and roles Functions, providers, runs, keys and credit belong to an account. A person can belong to several accounts and switches between them in the menu. | Role | Can | |---|---| | Owner | Everything a member can, plus members, invites, keys and credit. | | Member | Functions, providers and runs. | Anyone can sign up with an email address and make an account, which they own; make more from the account menu. An owner invites people under **Account**. An invite lasts 7 days. The invited person signs in with that address and joins. ## Pages The pictures show an example account, Northwind, in the dark theme. ### Home Runs in progress with their newest step, the last 7 days' runs, minutes and compute, how long the credit lasts at that rate, and each function's recent runs. A new account sees three first steps and function templates instead. ![Home: three live runs with their newest step, then the last 7 days' runs, run minutes, compute and credit](/img/console-home.webp) ### Runs Every run, newest first. Filter by state or function, or type to filter the loaded runs by prompt, reference or failure. Paste a run id to open it. ![Runs: each run with its function, failure if any, prompt, who started it, how long it took and what it cost](/img/console-runs.webp) ### A run A timeline of the run: the queue, the VM's start, setup, then each tool call. Below it are the steps and the transcript. Beside them are the run's time, cost, output files and model tokens. A queued or running run updates by itself; its log arrives every 30 seconds. **Replay** plays the log back at 8× speed. **Output** lists the files with previews. ![A running run: the timeline, the prompt and setup steps, and its time, cost and output files so far](/img/console-run.webp) ### Functions Each function shows its input, machine, agent and output files, then **Run**, **Runs**, **Versions** and **Definition**. **Run** takes a prompt and an input folder, and shows the same run as a `jinn` command, `curl`, TypeScript and Go. **Edit** publishes a new version from a form or from JSON. ![A function: its input folder, machine, agent and output files, then the run form beside the same run as code](/img/console-function.webp) **Versions** shows what changed between versions and how each one did. ![Versions: each version with its changes and success rate, and the definition's diff against the version before](/img/console-versions.webp) ### Providers Providers and their versions. **Check key** lists the models a key can use. ### Account Credit and balance, the price of each size, members and invites, API keys, the webhook public key, and 30 days of usage. ![Account: the available credit, balance and credit line, the price of each size, and Add credit](/img/console-account.webp) ## Keys and search Press `⌘K` (`Ctrl+K`) or `/` on any page to jump to a run, a function or a page. In lists, `j` and `k` move and `Enter` opens. On a run, `j` and `k` move between steps and `r` replays it. ![Search: typing "review" lists the review-pr function and its recent runs](/img/console-search.webp) The console is dark or light, as your system is set. To choose, open the menu under your initials and pick System, Dark or Light. ## The CLI `jinn login` opens a page in the console that shows a code. Check that your terminal shows the same code, then approve. The CLI gets an admin key for the account you choose, named after the machine. ## Credit Runs are paid from the account's balance, at the price on [Pricing](/pricing). A run starts while the balance is above the account's minimum (its credit line). A run that has started stops, with `credit`, once the balance is more than $10 below it. Usage lists each run's minutes and memory. Owners add credit under **Account**.