Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,34 @@ Apify CLI is the command-line tool for creating, developing, and deploying [Apif
- Manage secret environment variables used by your Actors
- Works with any programming language — Actors run as Docker containers on the platform

## Agent skill

This repo ships an [agent skill](./skills/apify/SKILL.md) that teaches AI coding agents (Claude Code, Cursor, etc.) how to work with the Apify CLI reliably. You can print the skill straight from the CLI — it always matches your installed version:

```bash
apify help --skills
```

If you'd rather have the skill installed persistently, `apify help --skills` prints a valid `SKILL.md` that you can redirect into your agent's skills directory. The location depends on the agent:

### Claude Code

```bash
mkdir -p ~/.claude/skills/apify
apify help --skills > ~/.claude/skills/apify/SKILL.md
```

### Codex and other agents

Codex and most other agents follow the [Agent Skills open standard](https://developers.openai.com/codex/skills), which loads skills from `.agents/skills` (per repo) or `~/.agents/skills` (per user):

```bash
mkdir -p ~/.agents/skills/apify
apify help --skills > ~/.agents/skills/apify/SKILL.md
Comment on lines +29 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the skill should be named more specific, e.g.: ~/.claude/skills/apify-cli and ~/.agents/skills/apify-cli or even apify-cli-agentic-guide or apify-cli-agentic-help.

```

That copy is a snapshot; re-run it after upgrading the Apify CLI to refresh it.

## Quick start

1. **Install the CLI** (macOS / Linux):
Expand All @@ -29,15 +57,14 @@ Apify CLI is the command-line tool for creating, developing, and deploying [Apif
apify login
```

3. **Create, run, and deploy** your first Actor:
3. **Run an Actor** on the Apify cloud:

```bash
apify create # it will walk you through an interactive wizard
cd my-actor
apify run
apify push
apify call apify/hello-world --output-dataset
```

This runs the public [`apify/hello-world`](https://apify.com/apify/hello-world) Actor and prints its results. To build your own Actor instead, run `apify create` and follow the interactive wizard.

## Installation

### macOS / Linux (bundle, recommended)
Expand Down
13 changes: 9 additions & 4 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ DESCRIPTION
Prints out help about a command, or all available commands.

USAGE
$ apify help [commandString]
$ apify help [commandString] [--skills]

ARGUMENTS
commandString The command to get help for.

FLAGS

@DaveHanns DaveHanns Jun 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: the --skills read as if it should list all available skills or provide a skills for a certain command. It is a bit misleading.

What about --agent-guide?
-> apify help --agent-guide

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, to be at least in sync with mcpc, it should be singular --skill.
https://apify.slack.com/archives/C0ANU2AFS3D/p1782748998055599?thread_ts=1782728951.631509&cid=C0ANU2AFS3D

--skills Print the Apify CLI agent skill (guidance for driving
`apify` from agents).
```

##### `apify upgrade`
Expand Down Expand Up @@ -782,7 +786,8 @@ FLAGS
DESCRIPTION
Executes Actor remotely using your authenticated account.
Reads input from local key-value store by default.
Inspect the input schema first with "apify actors info <actor> --input".
To inspect the input schema before creating a JSON input, use "apify actors

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nit: is this really better? 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my suggestion, so I can answer: jumping from "executes" and "reads" to an imperative creates some dissonance. We still have human readers :D

info <actor> --input".

USAGE
$ apify actors call [actorId] [-b <value>]
Expand All @@ -799,8 +804,8 @@ FLAGS
-b, --build=<value> Tag or number of the build to
run (e.g. "latest" or "1.2.34").
-i, --input=<value> Optional inline JSON object
input for the Actor. Wrap the JSON in quotes to avoid
shell parsing issues. For JSON files, use --input-file.
input for the Actor. To avoid shell parsing issues, wrap
the JSON in quotes. For JSON files, use --input-file.
-f, --input-file=<value> Optional path to a file with
JSON input to be given to the Actor. The file must be a
valid JSON file. You can also specify `-` to read from
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"prepare": "husky"
},
"files": [
"dist"
"dist",
"skills"
],
"bin": {
"actor": "./dist/actor.js",
Expand Down
4 changes: 4 additions & 0 deletions scripts/build-cli-bundles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const entryPoints = [
// target below to the matching `@napi-rs/keyring-<platform>` subpackage so Bun's `--compile`
// embeds that one native `.node`. Must match the specifier in `src/lib/credentials.ts`.
const KEYRING_PLACEHOLDER = '__APIFY_KEYRING_NATIVE_SUBPACKAGE__';
const APIFY_CLI_SKILL = readFileSync(new URL('../skills/apify/SKILL.md', import.meta.url), 'utf-8');

// Maps the compiled (os, arch, libc) to the napi-rs keyring subpackage that ships its `.node`.
// `supportedArchitectures` (pnpm-workspace.yaml) forces all of these into node_modules at build
Expand Down Expand Up @@ -110,6 +111,9 @@ for (const entryPoint of entryPoints) {
files: {
[entryPoint]: lines.join('\n'),
},
define: {
__APIFY_CLI_SKILL__: JSON.stringify(APIFY_CLI_SKILL),
},
outdir: fileURLToPath(new URL(`../bundles/fat-clis`, import.meta.url)),
conditions: 'node',
target: 'bun',
Expand Down
111 changes: 111 additions & 0 deletions skills/apify/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
name: apify-cli
description: Patterns for invoking the Apify CLI (`apify`) from agents. Covers authentication, creating/running/pushing Actors, calling Actors in the cloud, and reading results from datasets and key-value stores.
---

## Start here

Run `apify -h` first to see the available commands and global options, then `apify <command> -h` (e.g. `apify call -h`) for the args and flags of a specific command. This is the source of truth — prefer it over assumptions.

## Non-interactive use

Many commands prompt when run interactively. To run without prompts, pass every required argument and flag explicitly:

- `apify create <name> --template <template>` — skip the create wizard.
- `apify init <name>` — skip the init prompt.
- `-y` / `--yes` on destructive commands (`apify actors rm`, etc.) — auto-confirm.
- `apify login --token <token>` — log in without the interactive token prompt.

If a command's help shows an "interactive note", it lists exactly which flags make it non-interactive.

## Auth

- Preferred for automation: set `APIFY_TOKEN` in the environment (no `apify login` needed). Get a token at https://console.apify.com/settings/integrations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: this might be potentially dangerous. If user, for any reason, has the APIFY_TOKEN already present in their environment, it would use it implicitly and possibly silently.

Overall, we should, instead, refer to https://apify.com/auth.md here.

Additional instructions on authentication bellow are fine, but also not ideal as we are going to have 2 sources of truth. The auth.md should have a CLI section instead (working on one currently).

- Or persist a session once: `apify login --token <token>`.
- Verify auth: `apify info` (prints the logged-in user; non-zero exit / error if not authenticated).
- Print the stored token: `apify auth token`.

## Structured output

- `--json` is supported on most list/info commands (`apify actors ls --json`, `apify actors info <id> --json`, `apify datasets info <id> --json`, `apify runs ls --json`, etc.). Use it and parse with `jq`; don't scrape the human table.
- List commands paginate — control with `--limit` / `--offset` (and `--desc`).
- Dataset items: `apify datasets get-items <datasetId> --format json`. Use `--limit` / `--offset`.

## Core workflows

**Discover Actors in the Apify Store**

Before assuming an Actor name or scripting a raw Store query, search for an existing Actor:

```sh
apify actors search "jobs scraper" --json # no auth required
apify actors search "ai" --pricing-model FREE --sort-by popularity --limit 5
```

Run `apify actors search -h` to see the available filters (pricing model, category, username, sort order, pagination) and their accepted values.

Pricing matters: prefer a `FREE` Actor when one covers the task. Check an Actor's pricing before running it with `apify actors info <actor> --json` (look at `currentPricingInfo`).

@DaveHanns DaveHanns Jun 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer a FREE Actor when one covers the task.

Might lead to agents picking FREE over well supported, popular and well rated Actors. We might need more clarification that it is a combination of all above that matters.


**Develop and deploy a local Actor**

```sh
apify create my-actor --template <template> # or run `apify create` and pick interactively
# template names: https://raw.githubusercontent.com/apify/actor-templates/master/templates/manifest.json
cd my-actor
apify run # run locally; --input / --input-file - for input
apify push # build & deploy to the platform
```

**Run an Actor in the cloud and get results**

```sh
apify call apify/website-content-crawler -i '{"startUrls":[{"url":"https://example.com"}]}' --json
# or non-blocking: apify actors start <actor> --json (returns run details immediately)
# inspect input schema first: apify actors info <actor> --input
```

Non-obvious `apify call` flags: `-f -` reads input from stdin; `-o`/`--output-dataset` prints the result dataset. Run `apify call -h` for the full list.

**Wait for and inspect runs/builds**

```sh
apify runs ls --json
apify runs info <runId> --json
apify runs wait <runId> # block until finished
apify runs log <runId>
apify builds wait <buildId>
```

**Storage**

```sh
apify datasets get-items <datasetId> --format json
apify key-value-stores get-value <storeId> <key>
apify key-value-stores set-value <storeId> <key> <value>
apify key-value-stores keys <storeId> --json
```

## Scheduling and recurring runs

For anything recurring or unattended (e.g. "run every 15 minutes"), use the Apify platform — **not** local `cron`, a `while` loop, or GitHub Actions. Apify Schedules run in the cloud, so they keep firing after your laptop, terminal, or agent session is shut down.

- Save a reusable input config as a **task**, then run it: `apify task run <taskId>`.
- There is no dedicated `apify schedules` command yet — manage schedules via `apify api` against the `schedules` endpoint, or in the Console (https://console.apify.com/schedules):

```sh
apify api GET schedules
apify api POST schedules -d '{"name":"jobs-every-15m","cronExpression":"*/15 * * * *","isEnabled":true,"actions":[{"type":"RUN_ACTOR","actorId":"<actorId>","runInput":{"body":"<json>","contentType":"application/json"}}]}'
```

## Escape hatch: `apify api`

Any platform capability without a dedicated command is reachable via the authenticated API wrapper (use this instead of hand-rolling `curl` against `api.apify.com` — it injects auth for you):

```sh
apify api --list-endpoints # discover endpoints (filter with -s "<tokens>")
apify api --describe "actor-runs/{runId}" # methods, summary, path params for an endpoint
apify api GET /v2/users/me # GET is the default method
apify api POST acts -d '<json>' -p '{"limit":1}' # -d body (use - for stdin), -p query params
```

The `v2/` prefix and leading slash are optional.
56 changes: 55 additions & 1 deletion src/commands/help.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,43 @@
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import chalk from 'chalk';

import { ApifyCommand, commandRegistry } from '../lib/command-framework/apify-command.js';
import { Args } from '../lib/command-framework/args.js';
import { Flags } from '../lib/command-framework/flags.js';
import { renderHelpForCommand, renderMainHelpMenu } from '../lib/command-framework/help.js';
import { useCommandSuggestions } from '../lib/hooks/useCommandSuggestions.js';
import { error } from '../lib/outputs.js';
import { error, simpleLog } from '../lib/outputs.js';

declare const __APIFY_CLI_SKILL__: string | undefined;

const SKILL_RELATIVE_PATH = join('skills', 'apify', 'SKILL.md');

function readApifySkill(): string | null {
if (typeof __APIFY_CLI_SKILL__ === 'string' && __APIFY_CLI_SKILL__) {
return __APIFY_CLI_SKILL__;
}

let dir = dirname(fileURLToPath(import.meta.url));

while (true) {
const candidate = join(dir, SKILL_RELATIVE_PATH);

if (existsSync(candidate)) {
return readFileSync(candidate, 'utf-8');
}

const parent = dirname(dir);

if (parent === dir) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: worth adding a comment here that that dirname of the root (/ in UNIX and C:\) returns the root itself.

return null;
}

dir = parent;
}
}

export class HelpCommand extends ApifyCommand<typeof HelpCommand> {
static override name = 'help' as const;
Expand All @@ -21,9 +54,30 @@ export class HelpCommand extends ApifyCommand<typeof HelpCommand> {
}),
};

static override flags = {
skills: Flags.boolean({
description: 'Print the Apify CLI agent skill (guidance for driving `apify` from agents).',
default: false,
}),
};

async run() {
const { commandString } = this.args;

if (this.flags.skills) {
const skill = readApifySkill();

if (!skill) {
error({ message: 'Could not find the Apify CLI skill file.' });

return;
}

simpleLog({ stdout: true, message: skill.trimEnd() });

return;
}

if (!commandString || commandString.toLowerCase().startsWith('help')) {
const helpMenu = renderMainHelpMenu(this.entrypoint);

Expand Down
6 changes: 6 additions & 0 deletions test/e2e/commands/help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,10 @@ describe.concurrent('[e2e] help command', () => {
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
expect(result.stdout).toContain('Executes Actor locally with simulated Apify environment variables.');
});

it('apify help --skills prints the Apify CLI agent skill', async () => {
const result = await runCli('apify', ['help', '--skills']);
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
expect(result.stdout).toContain('name: apify-cli');
});
});
Loading