> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fetchski-attract.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Call any method from the terminal

> Try any SDK method on your site with one Artisan command, and see exactly what it answers.

The Laravel SDK ships `attract:call`: it calls one SDK method on your site and prints the answer as
JSON. Use it to see what a method really answers before you write code against it, to check a key's
scopes, or to make a one-off change without writing a script.

```bash Terminal theme={"dark"}
php artisan attract:call blogPosts.listBlogs perPage=5
```

> [card] [Find the method](#find-the-method)
>
> `attract:list` names every method and its route.

> [card] [Pass its arguments](#pass-its-arguments)
>
> `name=value`, with lists and objects as JSON.

> [card] [Read the answer](#read-the-answer)
>
> Pretty JSON, ready for `jq`.

> [!NOTE]
>
> It runs in any Laravel app with `fetchski/attract-cms-laravel` installed and a site configured
> ([Installation](https://docs.fetchski-attract.com/laravel/installation.md), [Configuration](https://docs.fetchski-attract.com/laravel/configuration.md)). It uses the same key,
> so it can do exactly what your app can, and nothing more.

## Find the method

Every method has a key: its accessor and its name, `blogPosts.updateBlog`. List them with
`attract:list`:

```bash Terminal theme={"dark"}
php artisan attract:list
```

Narrow it to one accessor:

```bash Terminal theme={"dark"}
php artisan attract:list blogPosts
```

```text Output theme={"dark"}
+-----------------------------------+---------------------------------------------+----------------------------------+
| Key                               | Route                                       | Summary                          |
+-----------------------------------+---------------------------------------------+----------------------------------+
| blogPosts.archiveBlogs            | POST /@apostrophecms/blog/archive           | Archive blogs (a background job) |
| blogPosts.createBlog              | POST /@apostrophecms/blog                   | Create blog                      |
| blogPosts.getBlog                 | GET /@apostrophecms/blog/{_id}              | Get blog                         |
| blogPosts.listBlogs               | GET /@apostrophecms/blog                    | List blogs                       |
| blogPosts.updateBlog              | PATCH /@apostrophecms/blog/{_id}            | Update blog                      |
+-----------------------------------+---------------------------------------------+----------------------------------+
```

A misspelled accessor or key gets a suggestion (`No resource blogPost: did you mean blogPosts?`).

> [!TIP]
>
> Every Laravel reference page opens with its method's `attract:call` line, under **Call it from the
> terminal**: copy it, then change the values.

### See a method's arguments

`--json` prints each method's entry: its arguments, their types and which are required. With `jq`:

```bash Terminal theme={"dark"}
php artisan attract:list blogPosts --json | jq '.[] | select(.key == "blogPosts.updateBlog") | .args[] | {name, kind, required}'
```

The method's reference page says the same in words, with each argument's rules.

### Find an id first

Most methods take a document's `id`. Every list has a `…Pick` twin that answers only the fields you
name, for every document, so its ids are one short call away:

```bash Terminal theme={"dark"}
php artisan attract:call pages.listPagesPick fields=title,slug
```

```json Output theme={"dark"}
[
    {
        "id": "ckq3v8x5b0002:en:published",
        "title": "About Us",
        "slug": "/about-us"
    }
]
```

`fields` takes a comma list, or a JSON one (`'fields=["title","slug"]'`). More in
[Find an id](https://docs.fetchski-attract.com/find-an-id.md).

## Pass its arguments

Each argument is one `name=value` word after the key, named as the SDK names it:

```bash Terminal theme={"dark"}
php artisan attract:call blogPosts.getBlog id=clx8m2k4p0000a1b2c3d4e5f6 aposMode=draft
```

The value is read by the argument's type:

| Type | Write it as | Example |
| - | - | - |
| Text, an id, a slug | as it is | `slug=spring-update` |
| One of a set (enum) | one of the values its reference page lists | `aposMode=draft` |
| Whole number | digits | `perPage=5` |
| Number | digits, with a point if you need one | `price=49.5` |
| True or false | `true` / `false` (or `1` / `0`) | `renderAreas=true` |
| Date | `YYYY-MM-DD` | `publishedAt=2026-10-27` |
| Date and time | ISO 8601 | `from=2026-10-01T00:00:00Z` |
| List | JSON | `'ids=["clx8m2k4p0000a1b2c3d4e5f6"]'` |
| Object or model | JSON | `'messages={"completed":"Archived {{ count }}."}'` |
| File | a path, from your app's root | `file=storage/app/photo.jpg` |

**Quote a word that holds a space, JSON or a shell character.** Single quotes around the whole
`name=value` keep the shell out of it:

```bash Terminal theme={"dark"}
php artisan attract:call blogPosts.updateBlog id=clx8m2k4p0000a1b2c3d4e5f6 'title=Spring update' aposMode=draft
```

Leave out an optional argument and it isn't sent: the site's default applies.

> [accordion] When an argument is wrong
>
> Nothing is sent, and the command says what to fix:
>
> ```text Output theme={"dark"}
> ERROR  blogPosts.getBlog needs id (id=…). Its arguments: id, aposMode, aposLocale, renderAreas.
> ERROR  blogPosts.listBlogs takes no argument "limit". Its arguments: page, perPage, search, aposMode, aposLocale, renderAreas, slug.
> ERROR  perPage must be a whole number, got "five".
> ERROR  ids must be JSON (Syntax error), got ["clx8m2k4p0000a1b2c3d4e5f6".
> ```

> [accordion] Walking every page
>
> The `…All` methods (`blogPosts.listBlogsAll`) walk every page of a list into models, which the
> command doesn't offer: call the list method a page at a time (`page=2`), or its `…Pick` twin,
> which walks every page for the fields you name.

## Writes ask first

A method that changes the site asks before it sends anything, naming the site:

```text Output theme={"dark"}
blogPosts.updateBlog writes to the site acme. Call it? (yes/no) [no]
```

Answer `yes` to call it. In a script, or once you're sure, add `--force` to skip the question:

```bash Terminal theme={"dark"}
php artisan attract:call blogPosts.updateBlog id=clx8m2k4p0000a1b2c3d4e5f6 'title=Spring update' aposMode=draft --force
```

> [!WARNING]
>
> `--force` sends it at once. A write in published mode (the default) goes live: send `aposMode=draft` to
> change only the draft, and publish it from the CMS when it's right ([Drafts and locales](https://docs.fetchski-attract.com/drafts-and-locales.md)).

## Choose the site

With no option the command calls your default site (`FETCHSKI_ATTRACT_SITE`). When your app holds
several, name one with `--site=`:

```bash Terminal theme={"dark"}
php artisan attract:call blogPosts.listBlogs perPage=5 --site=acme
```

The slug is one under `sites` in `config/fetchski-attract.php`, or one stored with `attract:store-key`
when keys live in your database ([Key in the database](https://docs.fetchski-attract.com/laravel/key-in-database.md)).

## Read the answer

The answer is printed as pretty JSON, keyed as the site sends it (`_id`, not the SDK's `id`):

```json Output theme={"dark"}
{
    "pages": 3,
    "currentPage": 1,
    "results": [
        {
            "title": "Spring update",
            "slug": "spring-update",
            "_id": "clx8m2k4p0000a1b2c3d4e5f6:en:published"
        }
    ]
}
```

A call that works prints only its JSON, so it pipes straight into `jq`:

```bash Terminal theme={"dark"}
php artisan attract:call blogPosts.listBlogs perPage=50 | jq -r '.results[] | "\(._id)  \(.title)"'
```

| You see | It means |
| - | - |
| JSON | The method's answer |
| `{ "jobId": "…" }` | A background job started: the site carries on with it (a site API key can't read a job, so a note on stderr says to read the documents themselves) |
| `… answered nothing (done).` | The method has no answer, and it worked |
| `Saved storage/app/blog/….md.` | `--save` wrote the file (below) |
| `ERROR …` | Nothing changed, or the site refused it: the line says which, and why |

The command exits `0` when the call worked and `1` when it didn't, so a script can stop on a failure.

### Save a file

The blog's templates (`blogPosts.getBlogMarkdownTemplate`, `blogPosts.getBlogMdxTemplate`) answer a
file. Add `--save=` to write it instead of printing its JSON: a folder (ending in `/`) keeps the name
the site gives it, any other path is the file.

```bash Terminal theme={"dark"}
php artisan attract:call blogPosts.getBlogMarkdownTemplate --save=storage/app/blog/
```

```text Output theme={"dark"}
INFO  Saved storage/app/blog/template_blog_post.md.
```

Missing folders are made, and an existing file is replaced. `--save` on a method that answers no file
stops before calling the site, and says so.

### When the site refuses

The site's answer is passed on as it is, with the route:

```text Output theme={"dark"}
ERROR  Site "acme" answered GET /api/v1/@apostrophecms/blog/nope with 404 notfound.
```

A `403` names the scope the key lacks ([Required scopes](https://docs.fetchski-attract.com/scopes.md)); a `401` means the key itself was
refused ([Authentication](https://docs.fetchski-attract.com/authentication.md)).

## Worked examples

> [tab] A read
>
> The five newest posts, drafts included (a draft read needs the type's write scope):
>
> ```bash Terminal theme={"dark"}
> php artisan attract:call blogPosts.listBlogs perPage=5 aposMode=draft
> ```
>
> One post by its id:
>
> ```bash Terminal theme={"dark"}
> php artisan attract:call blogPosts.getBlog id=clx8m2k4p0000a1b2c3d4e5f6
> ```

> [tab] A draft write
>
> Change a post's title and excerpt in its draft only, leaving the live post as it is:
>
> ```bash Terminal theme={"dark"}
> php artisan attract:call blogPosts.updateBlog \
>   id=clx8m2k4p0000a1b2c3d4e5f6 \
>   aposMode=draft \
>   'title=Spring update' \
>   'excerpt=What changed this season, in two minutes.'
> ```
>
> It asks first; the answer is the draft as it now stands. Publish it when it's ready:
>
> ```bash Terminal theme={"dark"}
> php artisan attract:call blogPosts.publishBlog id=clx8m2k4p0000a1b2c3d4e5f6
> ```

> [tab] A bulk job
>
> Archive several images at once, without asking:
>
> ```bash Terminal theme={"dark"}
> php artisan attract:call media.archiveImages 'ids=["clx8m2k4p0000a1b2c3d4e5f6","clx8m2k4p0000a1b2c3d4e5f7"]' --force
> ```
>
> ```json Output theme={"dark"}
> {
>     "jobId": "clx9a7d2q0000b4c5d6e7f8g9"
> }
> ```
>
> The site runs the job in the background: the images leave the media library as it works through
> them. Every id must name an image your key may edit, or nothing starts: an id that names nothing
> (an image already archived and deleted, a typo) is refused at once, with the ids that matched
> nothing.

> [tab] An upload
>
> Upload a file from your app's `storage/app` folder:
>
> ```bash Terminal theme={"dark"}
> php artisan attract:call attachments.uploadAttachment file=storage/app/photo.jpg
> ```
>
> Uploads are signed by their SHA-256 when your key's signing mode asks for it; the SDK does that for
> you ([Signing requests](https://docs.fetchski-attract.com/signing.md)).

## Related

> [card] [Artisan commands](https://docs.fetchski-attract.com/laravel/artisan-commands.md)
>
> Every command the package adds, with its options.

> [card] [Calling the API](https://docs.fetchski-attract.com/laravel/calling-the-api.md)
>
> The same calls in your app's code.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.