agentleFS
JustSteveKingJustSteveKing/api-skill/SKILL.md

api-skill

Skill82 starsChanged 7 months agoMITView on GitHub

Provides opinionated best practices and patterns for building production-ready REST APIs using Laravel. Use this skill when designing API endpoints, implementing resource controllers, or structuring JSON responses in a Laravel environment.

2 other projects use a version of this.

Adopted by

Most recently taken up by

  • Other versions2 projects use a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 2

    1. Kevin-Liu-01/Claude-of-Tanks★ 531same name
    2. techhorizonlabs/gearlist★ 2same name

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

The file

Show its rules and text

We split this skill into the rules it gives an agent

26 rules. About 0 are common, 10 are distinctive, and 16 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈10

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 16

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Always version from day one.

    line 67
  2. Very specific:

    Always use named routes, namespaced to their version (e.g.

    line 68
  3. Very specific:

    Every controller is a final single-action invokable class.

    line 75
  4. Very specific:

    Every state-mutating endpoint uses a Form Request.

    line 127
  5. Very specific:

    Always use Laravel's Eloquent API Resources to transform model data.

    line 199
  6. Very specific:

    Call JsonResource::withoutWrapping() in AppServiceProvider::boot() to disable the automatic data envelope globally.

    line 207
  7. Very specific:

    Always use Symfony's Response::HTTP_* constants.

    line 222
  8. Distinctive:

    All error responses follow RFC 9457 Problem Details.

    line 242
  9. Distinctive:

    All API routes are protected with Laravel Sanctum using stateless API tokens.

    line 283
  10. Very specific:

    All business logic lives in Action classes under app/Actions/{Resource}/.

    line 322
  11. Very specific:

    Every action that writes to the database must be wrapped in $this->database->transaction() using the injected DatabaseManager.

    line 354
  12. Very specific:

    Prefer non-blocking API responses.

    line 360
  13. Distinctive:

    Use judgement. If the client cannot function without the result, make it synchronous.

    line 409
  14. Distinctive:

    Always inject dependencies via the constructor.

    line 415
  15. Distinctive:

    If you know you need a class, declare it in the constructor.

    line 421
  16. Very specific:

    Always use simplePaginate().

    line 443
  17. Very specific:

    Never use paginate() on API endpoints.

    line 449
  18. Very specific:

    Every API route group must have throttle:api middleware.

    line 455
  19. Distinctive:

    Use Spatie Laravel Query Builder for all filterable list endpoints.

    line 467
  20. Distinctive:

    Test from the outside in. Drive tests through HTTP — assert on the response, not on internals.

    line 503
  21. Very specific:

    When a versioned endpoint is scheduled for removal, signal this to consumers using the Sunset HTTP header (RFC 8594).

    line 532
  22. Very specific:

    Create a Sunset middleware at app/Http/Middleware/Sunset.php:

    line 534
  23. Distinctive:

    Every PHP file in the project must meet these standards without exception:

    line 680
  24. Distinctive:

    Never use auto-increment integer IDs on API-facing models.

    line 713
  25. Very specific:

    Always use Laravel's HasUlids trait:

    line 715
  26. Very specific:

    Call Model::shouldBeStrict() in AppServiceProvider::boot().

    line 743

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

API Skill for Laravel Developers

This skill defines the exact patterns and rules for building scalable, reliable, and modern REST APIs in Laravel. All guidance here is prescriptive. When in doubt, follow the rule.


1. Route Organisation

Standalone APIs have no api prefix on any route. Routes live under routes/api/ as follows:

routes/
  api/
    routes.php       ← entry point, requires all resource files
    auth.php
    posts.php        ← one file per resource
    users.php

routes/api/routes.php loads in each resource using a prefix and naming:

<?php

declare(strict_types=1);

use Illuminate\Support\Facades\Route;

Route::as('auth:')->group(base_path(
    path: 'routes/api/auth.php',
));

Route::as('posts:')->group(base_path(
    path: 'routes/api/posts.php',
));

Each resource file owns its own version prefix. This keeps versioning explicit and debuggable per resource:

<?php

declare(strict_types=1);

use App\Http\Controllers\Posts;
use Illuminate\Support\Facades\Route;

Route::prefix('v1/posts')->middleware(['auth:sanctum', 'throttle:api'])->group(function (): void {
    Route::get('/', Posts\V1\IndexController::class)->name('v1:index');
    Route::post('/', Posts\V1\StoreController::class)->name('v1:store');
    Route::get('/{post}', Posts\V1\ShowController::class)->name('v1:show');
    Route::put('/{post}', Posts\V1\UpdateController::class)->name('v1:update');
    Route::delete('/{post}', Posts\V1\DestroyController::class)->name('v1:destroy');
});
  • Always version from day one.
  • Always use named routes, namespaced to their version (e.g. posts:v1:index and posts:v1:store).
  • The throttle:api middleware must always be present.

2. Single-Action Invokable Controllers

Every controller is a final single-action invokable class. No resourceful controllers. No multiple methods per class. Just an invokable action and a constructor for any dependency injection.

Controllers live under app/Http/Controllers/{Resource}/{Version}/:

app/Http/Controllers/Posts/V1/
  IndexController.php
  ShowController.php
  StoreController.php
  UpdateController.php
  DestroyController.php

Dependencies are always injected via the constructor. Never use Facades, app(), or resolve() inside a controller. The __invoke method handles the request and returns a response:

<?php

declare(strict_types=1);

namespace App\Http\Controllers\Posts\V1;

use App\Actions\Posts\StorePostAction;
use App\Http\Requests\Posts\V1\StoreRequest;
use App\Http\Resources\PostResource;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

final class StoreController
{
    public function __construct(
        private readonly StorePostAction $action,
    ) {}

    public function __invoke(StoreRequest $request): JsonResponse
    {
        $post = $this->action->handle(
            payload: $request->payload(),
        );

        return new JsonResponse(
            data: new PostResource($post),
            status: Response::HTTP_CREATED,
        );
    }
}

3. Form Requests and Payloads (DTOs)

Every state-mutating endpoint uses a Form Request. Form Requests live under app/Http/Requests/{Resource}/{Version}/.

The Form Request must expose a payload() method that returns a typed DTO from app/Http/Payloads/. This keeps controllers free of array handling and makes the data contract explicit:

<?php

declare(strict_types=1);

namespace App\Http\Requests\Posts\V1;

use App\Http\Payloads\Posts\StorePayload;
use Illuminate\Foundation\Http\FormRequest;

final class StoreRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'title'   => ['required', 'string', 'max:255'],
            'content' => ['required', 'string'],
        ];
    }

    public function payload(): StorePayload
    {
        return new StorePayload(
            title:   $this->string('title')->toString(),
            content: $this->string('content')->toString(),
            userId:  $this->user()->id,
        );
    }
}

Payloads (DTOs) are plain PHP objects. They have a typed constructor and a toArray() method that returns what Eloquent expects. They live in app/Http/Payloads/:

<?php

declare(strict_types=1);

namespace App\Http\Payloads\Posts;

final class StorePayload
{
    public function __construct(
        public readonly string $title,
        public readonly string $content,
        public readonly string $userId,
    ) {}

    public function toArray(): array
    {
        return [
            'title'   => $this->title,
            'content' => $this->content,
            'user_id' => $this->userId,
        ];
    }
}

4. API Resources

Always use Laravel's Eloquent API Resources to transform model data. Generate them with the --json-api CLI flag:

php artisan make:resource PostResource --json-api

Resources live under app/Http/Resources/. They define the contract for what consumers receive. Never return raw models or plain arrays from a controller.

Call JsonResource::withoutWrapping() in AppServiceProvider::boot() to disable the automatic data envelope globally. This ensures resources serialise consistently whether returned directly from a controller or encoded inside a JsonResponse:

use Illuminate\Http\Resources\Json\JsonResource;

public function boot(): void
{
    JsonResource::withoutWrapping();
}

5. HTTP Status Codes

Always use Symfony's Response::HTTP_* constants. Never use bare integers. This makes intent readable at a glance:

ScenarioConstant
Successful readResponse::HTTP_OK
Resource createdResponse::HTTP_CREATED
Accepted for background processingResponse::HTTP_ACCEPTED
Successful delete (no body)Response::HTTP_NO_CONTENT
Validation failedResponse::HTTP_UNPROCESSABLE_ENTITY
UnauthenticatedResponse::HTTP_UNAUTHORIZED
ForbiddenResponse::HTTP_FORBIDDEN
Not foundResponse::HTTP_NOT_FOUND
Server errorResponse::HTTP_INTERNAL_SERVER_ERROR

Import: use Symfony\Component\HttpFoundation\Response;


6. Error Handling — RFC 9457 Problem Details

All error responses follow RFC 9457 Problem Details. The response shape is:

{
    "type": "https://example.com/problems/validation-error",
    "title": "Validation Error",
    "status": 422,
    "detail": "The given data was invalid.",
    "errors": {
        "title": ["The title field is required."]
    }
}

RFC 9457 also requires responses to be served with Content-Type: application/problem+json, not application/json. Enforce both the shape and the header through a single ProblemResponse class that implements Laravel's Responsable interface, living at app/Http/Responses/ProblemResponse.php. Every exception handler closure returns a ProblemResponse — no raw arrays, no ad-hoc JsonResponse construction:

return new ProblemResponse(
    type:   'https://example.com/problems/not-found',
    title:  'Not Found',
    status: Response::HTTP_NOT_FOUND,
    detail: 'The requested resource could not be found.',
);

The exception handler in bootstrap/app.php is responsible for rendering all exceptions. No exception should ever produce an HTML response on an API route. At minimum, the following must be handled explicitly:

ExceptionStatusProblem type slug
ValidationException422validation-error
AuthenticationException401unauthenticated
AuthorizationException403forbidden
ModelNotFoundException404not-found
Throwable (catch-all)500server-error

The catch-all ensures nothing falls through to Laravel's default HTML error page. See references/CONVENTIONS.md for the full ProblemResponse implementation and handler.


7. Authentication — Laravel Sanctum (Stateless Tokens)

All API routes are protected with Laravel Sanctum using stateless API tokens. Never use session-based authentication for API routes.

  • Middleware: auth:sanctum
  • Token abilities/scopes are used to restrict what a token can do
  • Registration and login flows are synchronous — token issuance must complete in the same request
// Protecting routes
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function (): void {
    // ...
});

// Checking abilities in a controller/action
$request->user()->tokenCan('posts:create');

8. Authorization — Policies

Authentication (section 7) confirms who the user is. Authorization confirms what they can do. Use Laravel Policies for resource-level access control.

Authorization belongs in the Form Request's authorize() method — not in the controller, not in the Action:

public function authorize(): bool
{
    return $this->user()->can('update', $this->route('post'));
}

Policies live in app/Policies/. A failed authorization throws an AuthorizationException, which the exception handler (section 6) renders as a 403 Forbidden Problem Details response.

Actions operate on data that has already been validated and authorized. Never put policy checks inside an Action.


9. The Action Pattern

All business logic lives in Action classes under app/Actions/{Resource}/. Controllers orchestrate, Actions execute. One action per operation.

Actions are injected into controllers via the constructor. They receive a DTO and return a model or value:

<?php

declare(strict_types=1);

namespace App\Actions\Posts;

use App\Http\Payloads\Posts\StorePayload;
use App\Models\Post;
use Illuminate\Database\DatabaseManager;

final class StorePostAction
{
    public function __construct(
        private readonly DatabaseManager $database,
    ) {}

    public function handle(StorePayload $payload): Post
    {
        return $this->database->transaction(
            callback: fn (): Post => Post::query()->create(
                attributes: $payload->toArray(),
            ),
        );
    }
}

Every action that writes to the database must be wrapped in $this->database->transaction() using the injected DatabaseManager.


10. Background Jobs — Non-Blocking Requests

Prefer non-blocking API responses. When an operation can be deferred, dispatch a job and return 202 Accepted:

<?php

declare(strict_types=1);

namespace App\Actions\Posts;

use App\Http\Payloads\Posts\StorePayload;
use App\Jobs\Posts\StorePostJob;

final class StorePostAction
{
    public function handle(StorePayload $payload): void
    {
        dispatch(new StorePostJob($payload));
    }
}
// Controller
return new JsonResponse(status: Response::HTTP_ACCEPTED);

When a job receives an Eloquent model, use the SerializesModels trait alongside Queueable. This stores the model's class and key on the queue rather than the full serialized object, and re-fetches it fresh when the job runs. Because the trait restores model properties via __wakeup(), model constructor properties must not be readonly:

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\SerializesModels;

final class DestroyPostJob implements ShouldQueue
{
    use Queueable;
    use SerializesModels;

    public function __construct(
        private Post $post, // not readonly — SerializesModels rehydrates via __wakeup()
    ) {}
}

Exceptions to non-blocking:

  • User registration — the user needs a token immediately.
  • Login / token issuance — must be synchronous.
  • Any operation where the response body is needed by the client to proceed.

Use judgement. If the client cannot function without the result, make it synchronous.


11. Dependency Injection

Always inject dependencies via the constructor. Never use:

  • Facades (unless there is no alternative.)
  • app() helper
  • resolve() helper

If you know you need a class, declare it in the constructor. Laravel's container will resolve it automatically.

// Correct
final class StoreController
{
    public function __construct(
        private readonly StorePostAction $action,
    ) {}
}

// Wrong — never do this inside a method
public function __invoke(): JsonResponse
{
    $action = app(StorePostAction::class); // ❌
}

12. Pagination

Always use simplePaginate(). It avoids the expensive COUNT(*) query of paginate() and keeps the response lean:

Post::query()->simplePaginate(perPage: 15);

Never use paginate() on API endpoints.


13. Rate Limiting

Every API route group must have throttle:api middleware. No endpoint is ever unthrottled. Define rate limiters in App\Providers\AppServiceProvider (or a dedicated RateLimitServiceProvider):

RateLimiter::for('api', function (Request $request): Limit {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

14. Query Filtering — Spatie Laravel Query Builder

Use Spatie Laravel Query Builder for all filterable list endpoints. Never build raw query strings manually:

<?php

declare(strict_types=1);

namespace App\Http\Controllers\Posts\V1;

use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\QueryBuilder;

final class IndexController
{
    public function __invoke(): AnonymousResourceCollection
    {
        $posts = QueryBuilder::for(Post::class)
            ->allowedFilters([
                AllowedFilter::exact('status'),
                AllowedFilter::partial('title'),
            ])
            ->allowedSorts(['created_at', 'title'])
            ->simplePaginate(perPage: 15);

        return PostResource::collection($posts);
    }
}

15. Testing

Test from the outside in. Drive tests through HTTP — assert on the response, not on internals. Cover both the happy path and the unhappy paths.

it('creates a post and returns 201', function (): void {
    $user = User::factory()->create();

    $this->actingAs($user)->postJson('/v1/posts', [
        'title'   => 'Hello World',
        'content' => 'This is the content.',
    ])->assertStatus(
        Response::HTTP_CREATED,
    )->assertJsonPath('title', 'Hello World');
});

it('returns 422 when title is missing', function (): void {
    $user = User::factory()->create();

    $this->actingAs($user)->postJson('/v1/posts', [
        'content' => 'Missing title.',
    ])->assertStatus(
        Response::HTTP_UNPROCESSABLE_ENTITY,
    )->assertJsonPath('status', 422)->assertJsonPath('title', 'Validation Error');
});

16. API Versioning — Sunset Middleware

When a versioned endpoint is scheduled for removal, signal this to consumers using the Sunset HTTP header (RFC 8594). This gives clients advance warning to migrate before the endpoint disappears.

Create a Sunset middleware at app/Http/Middleware/Sunset.php:

<?php

declare(strict_types=1);

namespace App\Http\Middleware;

use Closure;
use DateTimeImmutable;
use DateTimeInterface;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

final class Sunset
{
    public function handle(Request $request, Closure $next, string $date): Response
    {
        $response = $next($request);

        $response->headers->set(
            'Sunset',
            (new DateTimeImmutable($date))->format(DateTimeInterface::RFC7231),
        );

        return $response;
    }
}

Apply it per route group in the resource file when v1 has a known deprecation date:

Route::prefix('v1/posts')
    ->middleware(['auth:sanctum', 'throttle:api', 'sunset:2026-12-31'])
    ->group(function (): void {
        // v1 routes — sunset date is communicated on every response
    });

The v2 routes live in the same resource file without the middleware:

Route::prefix('v2/posts')
    ->middleware(['auth:sanctum', 'throttle:api'])
    ->group(function (): void {
        // v2 routes
    });

Register the middleware alias in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->alias([
        'sunset' => \App\Http\Middleware\Sunset::class,
    ]);
})

17. Forcing JSON Responses

Laravel's exception handler only returns JSON when $request->expectsJson() is true — which checks for the Accept: application/json header. If a client omits it, some exceptions (particularly those thrown before the handler fires, such as middleware failures) can still return HTML.

Fix this with a ForceJsonResponse middleware at app/Http/Middleware/ForceJsonResponse.php that sets the header on every inbound request:

<?php

declare(strict_types=1);

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

final class ForceJsonResponse
{
    public function handle(Request $request, Closure $next): Response
    {
        $request->headers->set('Accept', 'application/json');

        return $next($request);
    }
}

Register it as the first middleware on all API routes in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->alias([
        'sunset'            => \App\Http\Middleware\Sunset::class,
        'force.json'        => \App\Http\Middleware\ForceJsonResponse::class,
    ]);
})

Apply it at the top of every resource route group:

Route::prefix('v1/posts')
    ->middleware(['force.json', 'auth:sanctum', 'throttle:api'])
    ->group(function (): void {
        // ...
    });

18. CORS

A standalone API will be called from origins other than its own domain. Laravel includes HandleCors middleware out of the box, backed by config/cors.php.

For a standalone API, set paths to ['*'] — there is no web prefix to protect:

// config/cors.php
return [
    'paths'                    => ['*'],
    'allowed_methods'          => ['*'],
    'allowed_origins'          => ['*'], // lock this down per environment
    'allowed_origins_patterns' => [],
    'allowed_headers'          => ['*'],
    'exposed_headers'          => [],
    'max_age'                  => 0,
    'supports_credentials'     => false,
];

In production, replace allowed_origins: ['*'] with an explicit list of trusted origins. Drive it from an environment variable so it can vary across environments without a code change:

'allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', '*')),

HandleCors is applied globally by Laravel's default middleware stack, so no per-route changes are needed.


19. Code Quality Standards

Every PHP file in the project must meet these standards without exception:

  • declare(strict_types=1) on every file, always the first statement after the opening tag
  • final on every class — controllers, actions, payloads, jobs, middleware, resources — unless explicitly designed for extension
  • match over if/elseif chains and nested ternaries wherever a value is being selected
  • Named arguments on constructor calls and method calls with multiple parameters — this is already demonstrated throughout the examples and is required, not optional
  • Full type coverage — typed properties, typed parameters, typed return types on every method
  • PSR-12 naming and formatting conventions throughout
<?php // ✓

declare(strict_types=1); // ✓ always first

namespace App\Actions\Posts;

final class StorePostAction // ✓ final
{
    public function handle(StorePayload $payload): Post // ✓ fully typed
    {
        return $this->database->transaction(
            callback: fn (): Post => Post::query()->create( // ✓ named argument
                attributes: $payload->toArray(),
            ),
        );
    }
}

20. Model Identifiers — ULIDs

Never use auto-increment integer IDs on API-facing models. Integer IDs expose record counts, enable enumeration attacks, and create ordering assumptions consumers should not rely on.

Always use Laravel's HasUlids trait:

<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Model;

final class Post extends Model
{
    use HasUlids;
}

Laravel handles ULID generation automatically. No migration changes beyond defining the primary key column as string (26 characters) are required:

$table->ulid('id')->primary();

21. Model Strictness

Call Model::shouldBeStrict() in AppServiceProvider::boot(). This enables three protections simultaneously:

  • Prevents lazy loading — any unloaded relationship accessed outside of with() throws immediately, surfacing N+1 queries during development
  • Prevents silently discarded attributes — assigning an attribute not in $fillable throws rather than silently ignoring it
  • Prevents accessing missing attributes — reading an attribute that doesn't exist on the model throws rather than returning null
use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::shouldBeStrict();

    JsonResource::withoutWrapping();

    RateLimiter::for('api', function (Request $request): Limit {
        return Limit::perMinute(60)->by(
            key: $request->user()?->id ?: $request->ip(),
        );
    });
}

These are development-time errors by design. Fix the underlying issue — don't suppress the exception.


22. Anti-patterns

The following are explicitly prohibited. If you find yourself reaching for any of these, stop and apply the correct pattern instead.

Anti-patternWhyWhat to do instead
Auto-increment integer primary keysExposes record counts, enables enumerationUse HasUlids (section 20)
Business logic in models (scopes excluded)Violates single responsibility, untestable in isolationUse Actions (section 9)
Multi-method or resourceful controllersObscures intent, harder to locate logicSingle-action invokable controllers (section 2)
Returning raw models or arrays from controllersLeaks schema, no transformation layerAlways use API Resources (section 4)
app(), resolve(), or Facades in controllers/actionsHides dependencies, harder to testConstructor injection (section 11)
DB::transaction() FacadeSame as above — hides the dependencyInject DatabaseManager (section 9)
paginate() on list endpointsRuns an expensive COUNT(*)Always simplePaginate() (section 12)
Unthrottled routesExposes all endpoints to brute force and abuseAlways throttle:api (section 13)
HTML error responses on API routesBreaks every API clientForceJsonResponse + full exception handler (sections 6, 17)
Skipping declare(strict_types=1)Silent type coercion bugsRequired on every file (section 19)
Authorization checks in ActionsActions receive already-authorized dataAuthorize in Form Request authorize() (section 8)

References

For detailed folder structure and naming conventions, see references/CONVENTIONS.md.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.