Running Artisan Commands Across Tenants: A Guide to tenants:run (stancl/tenancy) | Deploynix Laravel Blog
Back to Blog

Running Artisan Commands Across Tenants: A Guide to tenants:run (stancl/tenancy)

Sameh Elhawary · · 9 min read
Running Artisan Commands Across Tenants: A Guide to tenants:run (stancl/tenancy)

Running Artisan Commands Across Tenants: A Guide to tenants:run (stancl/tenancy)

If you run a multi-tenant Laravel app with stancl/tenancy, sooner or later you need to run an Artisan command for every tenant at once — migrate their databases, clear their caches, or regenerate a nightly report. The problem is that php artisan migrate only touches your central database. Each tenant gets left behind.

Plenty of developers go hunting for a tenants:artisan command to solve this. It doesn't exist. The command you actually want is tenants:run, and it wraps any registered Artisan command in each tenant's context. This guide walks through tenants:run, the dedicated tenant-aware commands, the programmatic API, and the gotchas that bite once you're past a handful of tenants.

Key takeaways

- The generic command is tenants:run {commandname} — not tenants:artisan. It runs any registered Artisan command inside each tenant's context. - Scope it with --tenants=ID (repeat the flag for several). Omit it and the command hits every tenant. - Forward arguments and options with --argument="key=value" and --option="key=value". - For migrations, seeds, and rollbacks, prefer the dedicated commands: tenants:migrate, tenants:seed, tenants:rollback. - It runs tenants sequentially. Once you pass a few hundred, queue a job per tenant instead of looping in one process.

What does tenants:run actually do?

The tenants:run command iterates over your tenants and, for each one, initializes tenancy, runs the command you named, then ends tenancy before moving on. Initializing tenancy is what swaps the database connection, cache prefix, and filesystem paths over to that tenant via the package's bootstrappers — so the wrapped command behaves exactly as it would inside a tenant request.

Here's the signature straight from the package source:

php artisan tenants:run {commandname}
    {--tenants=*}
    {--argument=*}
    {--option=*}

The commandname is any Artisan command that's already registered in your app. The simplest case takes no extra arguments at all:

# Clear the cache for every tenant
php artisan tenants:run cache:clear

Behind the scenes this is the same loop you'd write by hand: initialize tenant, call cache:clear, end tenant, repeat. The package just hides the boilerplate and the context switching.

How do I target specific tenants?

Use the --tenants option, repeating it once per tenant ID. Leave it off and the command runs for all tenants — which is convenient for cache clears but genuinely dangerous for anything destructive.

# One tenant
php artisan tenants:run cache:clear --tenants=acme

# Several tenants (repeat the flag)
php artisan tenants:run cache:clear --tenants=acme --tenants=globex

# All tenants (the default when --tenants is omitted)
php artisan tenants:run cache:clear

Get into the habit of scoping with --tenants while testing. A mistyped command name fails loudly for one tenant; a destructive command run against the default "all" can ruin your evening.

How do I pass arguments and options to the command?

This is where tenants:run trips people up. You can't just append flags to the wrapped command, because the parser would assign them to tenants:run itself. Instead, you pass them through with --argument and --option, each as a key=value pair.

php artisan tenants:run email:send \
    --tenants=acme \
    --argument="subject=Welcome" \
    --option="queue=1"

So a command you'd normally invoke as php artisan email:send "Welcome" --queue becomes the form above. Repeat --argument and --option for each value you need to forward. It's verbose, but it keeps the wrapper's flags and the inner command's flags from colliding.

When should I use the dedicated commands instead?

stancl/tenancy ships purpose-built commands for the operations you run most. They handle the right defaults for you — tenant migrations, for example, look in database/migrations/tenant automatically rather than your central migrations folder. Reach for these before tenants:run:

Command

What it does

Key options

tenants:migrate

Runs tenant migrations against each tenant database

--tenants=*, --path, --force

tenants:migrate-fresh

Wipes and re-migrates a tenant database (destructive)

--tenants=*

tenants:rollback

Reverses the last migration batch per tenant

--tenants=*

tenants:seed

Seeds each tenant database

--tenants=*, --force

tenants:list

Lists all tenants with their IDs and domains

tenants:run

Runs any other registered Artisan command per tenant

--tenants=<em>, --argument=</em>, --option=*

# Migrate every tenant database (use --force in production)
php artisan tenants:migrate --force

# Roll back the last batch for two tenants
php artisan tenants:rollback --tenants=acme --tenants=globex

# Wipe and re-migrate a single tenant (destructive!)
php artisan tenants:migrate-fresh --tenants=acme

# Seed every tenant non-interactively
php artisan tenants:seed --force

One detail worth burning into memory: these commands run non-interactively when invoked from a deploy hook or scheduler, so any command that normally asks "are you sure?" in production needs --force. Without it, tenants:migrate will hang or abort.

Can I run tenant commands from my own code?

Yes, and it's often cleaner than shelling out to Artisan. Every tenant exposes a run() method that initializes its context, executes a closure, then restores the previous context. For batches, tenancy()->runForMultiple() does the same across a collection.

<?php

use App\Models\Tenant;
use Illuminate\Support\Facades\Artisan;

// Run a closure inside one tenant's context
$tenant->run(function () {
    Artisan::call('cache:clear');
});

// Run across many tenants
tenancy()->runForMultiple(
    Tenant::all(),
    function (Tenant $tenant) {
        Artisan::call('migrate', ['--force' => true]);
    }
);

This is the right tool when the logic doesn't belong in an Artisan command at all — backfilling a column, recalculating balances, or warming a cache. You keep full control over error handling and you skip the overhead of booting a new command for each tenant.

How do I scale this past a few hundred tenants?

Every approach above is sequential. One process initializes tenant A, finishes, then moves to tenant B. That's fine for ten tenants and painful for ten thousand — a single slow query or failure stalls the whole run, and memory creeps up as Eloquent and the query log accumulate.

The fix is to stop looping in one process and dispatch a queued job per tenant instead. Each job runs independently, retries on its own, and spreads across your workers:

<?php

use App\Models\Tenant;
use App\Jobs\GenerateTenantReport;

Tenant::query()
    ->lazy()
    ->each(fn (Tenant $tenant) => GenerateTenantReport::dispatch($tenant));

The job itself initializes the tenant and does its work, exactly like a single iteration of the loop would:

<?php

class GenerateTenantReport implements ShouldQueue
{
    public function __construct(public Tenant $tenant) {}

    public function handle(): void
    {
        $this->tenant->run(function () {
            Artisan::call('report:generate');
        });
    }
}

Two things to watch as tenant counts grow. First, state bleed: the bootstrappers reset the database, cache, and filesystem between tenants, but any custom singleton holding tenant-specific data will leak unless you reset it too. Second, memory: prefer lazy() or cursor() over all() when iterating, so you don't hydrate every tenant model into memory at once.

Running tenant commands during a Deploynix deploy

When you deploy a multi-tenant Laravel app with Deploynix, your release hook is the natural home for tenant migrations. The ordering matters: migrate the central database first (it holds the tenants table), then migrate the tenants. A typical hook looks like this:

# Deploynix deploy hook (runs after each release)
php artisan migrate --force
php artisan tenants:migrate --force
php artisan tenants:run cache:clear

Because deploy hooks are non-interactive, every command needs --force. For recurring tenant work — nightly reports, cleanup, billing runs — register it with the scheduler in routes/console.php rather than the deploy hook, so it runs on a cadence instead of on every release:

<?php

use Illuminate\Support\Facades\Schedule;

Schedule::command('tenants:run report:generate')->daily();

That keeps deploys fast and idempotent while your per-tenant maintenance ticks along in the background on a server Deploynix already provisioned with a scheduler and queue worker.

Frequently asked questions

Is there a tenants:artisan command in stancl/tenancy?

No. There's no tenants:artisan command in stancl/tenancy. The generic command for running any registered Artisan command in each tenant's context is tenants:run {commandname}. The confusion is common because the feature is exactly what people imagine "tenants:artisan" would do.

Why does my plain php artisan migrate skip the tenants?

Because migrate runs against your central connection only. Tenant tables live in separate databases (or schemas) using migrations in database/migrations/tenant. Use php artisan tenants:migrate to apply those, and keep central and tenant migrations in their separate folders.

How do I run a command for just one tenant?

Pass the tenant's ID to the --tenants option, for example php artisan tenants:run cache:clear --tenants=acme. The flag is repeatable, so add it once per tenant when you want to target a specific subset rather than all of them.

Do I need --force for tenant migrations in production?

Yes. Tenant commands invoked from deploy hooks or the scheduler run non-interactively, so any command that normally prompts for confirmation in production — including tenants:migrate and tenants:seed — needs --force to proceed without hanging.

Should I use tenants:run or queued jobs?

Use tenants:run for one-off operations and small tenant counts. Once you're running across hundreds or thousands of tenants, dispatch a queued job per tenant instead — you get isolation, independent retries, and parallel execution across workers rather than one long sequential loop.

Wrapping up

Running Artisan commands across tenants comes down to one command and a few good habits: reach for tenants:run when no dedicated command exists, lean on tenants:migrate and friends for the common operations, always scope with --tenants while testing, and add --force for anything that runs unattended. Past a few hundred tenants, move the loop onto your queue.

Deploynix handles the servers, queue workers, and scheduler that make all of this run reliably in production. Deploy your Laravel app with Deploynix and wire your tenant migrations into a release hook in minutes.

Ready to deploy your Laravel app?

Deploynix handles server provisioning, zero-downtime deployments, SSL, and monitoring — so you can focus on building.

Get Started Free No credit card required

Related Posts