Use the CLI

Generate a course, wait for it, grade an answer, and check your balance from a shell or CI job with @tutorflow/cli 0.5.0, without writing HTTP calls.

After this guide an agent or a CI job can generate TutorFlow content from a shell with one command per step. The CLI, @tutorflow/cli 0.5.0, calls the same Content API routes as the earlier guides, waits for runs for you, and prints JSON when you ask.

Install

The CLI needs Node.js 18 or later.

0.5.0

Give it your key

If you already have a Content key from Quick Start, put it in the environment:

bash
export TUTORFLOW_API_KEY="tf_content_..."

tutorflow auth login --api-key tf_content_... saves it to ~/.tutorflow/config.json instead. With no key yet, tutorflow agent register --name "My Agent" --slug my-agent --email ops@example.com creates an account and saves its key. See Agent Account and Billing CLI.

The CLI finds your default classroom from the account, so you do not pass a classroom id.

Generate a course and wait for it

bash
tutorflow course generate "Spreadsheet basics for new office staff" \
  -n 1 --level beginner \
  --idempotency-key course:spreadsheets:v1 \
  --wait

--wait polls the run, prints each stage on stderr, and prints the result when it ends:

run 57b8...: running · outlining
run 57b8...: running · generating_lessons · 0/1 lessons
  runId: 57b8...
  status: completed
  creditsCharged: 6
  resourceId: 8e25...
  title: Spreadsheet Basics for New Office Staff
  editorUrl: https://tutorflow.io/org/my-agent/my-agent/courses/chat/3bc7.../lessons
  publicUrl: https://my-agent.tutorflow.ac/courses/spreadsheet-basics-for-new-office-staff

Without --wait, the command prints the run and returns. Follow it later with tutorflow run wait <runId>, or resume a failed course run with tutorflow run resume <runId>. Add --json to any command for one JSON line on stdout. For the course from Generate Your First Content:

bash
tutorflow run wait 6bff... --json
JSON
{
  "run": {
    "runId": "6bff...",
    "kind": "course",
    "status": "completed",
    "progress": { "completed": 1, "total": 1, "unit": "lesson" },
    "resourceId": "5d2a...",
    "creditsCharged": 6,
    "estimatedCredits": 6,
    "error": null
  },
  "resource": {
    "id": "5d2a...",
    "title": "Beginner Python for Adult Career Changers: Variables, Loops, and Functions",
    "editorUrl": "https://tutorflow.io/org/my-agent/my-agent/courses/chat/09cf.../lessons",
    "publicUrl": "https://my-agent.tutorflow.ac/courses/beginner-python-for-adult-career-changers-variables-loops-and-functions"
  }
}

run holds every field of the run and resource every field of the course; the example drops some for space.

Find and open what you made

bash
tutorflow course list
┌──────────────┬────────────────────────────────────────────────────────────────────────────┬────────┬────────────┐
│ ID           │ Title                                                                      │ Status │ Updated    │
├──────────────┼────────────────────────────────────────────────────────────────────────────┼────────┼────────────┤
│ 8e25...      │ Spreadsheet Basics for New Office Staff                                    │ PUBLIC │ 2026-10-16 │
│ 5d2a...      │ Beginner Python for Adult Career Changers: Variables, Loops, and Functions │ PUBLIC │ 2026-10-16 │
└──────────────┴────────────────────────────────────────────────────────────────────────────┴────────┴────────────┘
bash
tutorflow course open 5d2a... --json
JSON
{
  "id": "5d2a...",
  "editorUrl": "https://tutorflow.io/org/my-agent/my-agent/courses/chat/09cf.../lessons",
  "publicUrl": "https://my-agent.tutorflow.ac/courses/beginner-python-for-adult-career-changers-variables-loops-and-functions"
}

open works the same for slide, test, module, video, game, and simulation. In an interactive terminal it also opens a browser: publicUrl by default, or editorUrl with --editor or when publicUrl is null. With --json it only prints. With a test key both links are null, and open exits 1 with ENOPREVIEW.

Send publicUrl to a person who should look at the result, and editorUrl to one who should change it. publicUrl stays null until the page is public; Let a person look at it lists the change each type needs. share makes that change and prints the new publicUrl:

bash
tutorflow --json course share 5d2a... | jq -r .publicUrl

A video or test is shared as UNLISTED, so it opens by link but stays out of lists and search; add --listed for PUBLIC. A slide deck, game, simulation, or course becomes PUBLIC, and a module gets isPublic: true. tutorflow <type> visibility <id> <value> sets any other value, such as PRIVATE. publicUrl can still be null after share until a video is rendered, a game or simulation is built, or a course is published, and always with a test key.

Grade an answer

bash
tutorflow evaluate -t exact -q "What is 7 x 8?" -a "56" -e "56" --max-score 1 --learner-id learner-42
  id: d0ef...
  classroomId: 0fce...
  status: COMPLETED
  evaluationType: exact
  mode: sync
  isTerminal: true
  learner: {"externalId":"learner-42","name":null}
  conceptTags: []
  score: 1
  maxScore: 1
  normalizedScore: 100
  confidence: 1
  strengths: ["Answer matches the expected response."]
  mistakes: []
  suggestions: []
  feedbackSummary: Correct! Your answer matches the expected response.
  createdAt: 2026-10-16T18:55:13.223Z
  completedAt: 2026-10-16T18:55:13.268Z

An exact evaluation needs the reference answer in -e. Each evaluation costs 1 credit.

Check your balance

bash
tutorflow --json credits balance
JSON
{"credit":36,"overdraftCredit":0,"overdraftLimit":100,"availableCredit":36,"isPaymentFailed":false,"livemode":true}

In scripts and CI

  • Use --json. Results go to stdout as one line, errors to stderr as one line with code, status, and the API's apiCode.
  • The exit status is 0 on success, 1 for an API error, a failed run, or a --wait timeout, and 2 for a usage error.
  • Use a test key (tf_content_test_) in CI. It works in the sandbox classroom and spends nothing.
  • Always pass --idempotency-key on generate, so a retried job does not start a second run.

Reference