diff --git a/app/Actions/Automation/Trigger/DispatchPostTriggerAutomations.php b/app/Actions/Automation/Trigger/DispatchPostTriggerAutomations.php index 9c86fb39..b36316a1 100644 --- a/app/Actions/Automation/Trigger/DispatchPostTriggerAutomations.php +++ b/app/Actions/Automation/Trigger/DispatchPostTriggerAutomations.php @@ -5,6 +5,7 @@ namespace App\Actions\Automation\Trigger; use App\Actions\Automation\Run\AdvanceAutomationRun; +use App\Enums\Automation\Node\Type as NodeType; use App\Enums\Automation\Run\Status as RunStatus; use App\Enums\Automation\Status as AutomationStatus; use App\Enums\Automation\Trigger\Type as TriggerType; @@ -32,11 +33,13 @@ public function __invoke(Post $post, TriggerType $triggerType): void $automations = Automation::query() ->where('workspace_id', $post->workspace_id) ->where('status', AutomationStatus::Active) + ->where('trigger_type', $triggerType->value) ->get(); foreach ($automations as $automation) { - $triggerNode = collect($automation->nodes ?? [])->firstWhere('type', 'trigger'); - if (data_get($triggerNode, 'data.trigger_type') !== $triggerType->value) { + $triggerNode = collect($automation->nodes ?? [])->firstWhere('type', NodeType::Trigger->value); + + if ($triggerNode === null) { continue; } diff --git a/app/Console/Commands/Automation/FireScheduleTriggers.php b/app/Console/Commands/Automation/FireScheduleTriggers.php index 277b49e5..ce90cdde 100644 --- a/app/Console/Commands/Automation/FireScheduleTriggers.php +++ b/app/Console/Commands/Automation/FireScheduleTriggers.php @@ -6,6 +6,7 @@ use App\Actions\Automation\Trigger\FireScheduleTrigger; use App\Enums\Automation\Status; +use App\Enums\Automation\Trigger\Type as TriggerType; use App\Models\Automation; use Illuminate\Console\Attributes\Description; use Illuminate\Console\Attributes\Signature; @@ -19,12 +20,9 @@ public function handle(FireScheduleTrigger $fire): int { Automation::query() ->where('status', Status::Active) + ->where('trigger_type', TriggerType::Schedule->value) ->chunkById(50, function ($automations) use ($fire) { foreach ($automations as $automation) { - $triggerNode = collect($automation->nodes ?? [])->firstWhere('type', 'trigger'); - if (($triggerNode['data']['trigger_type'] ?? null) !== 'schedule') { - continue; - } $fire($automation); } }); diff --git a/app/Models/Automation.php b/app/Models/Automation.php index b81fd554..8189913b 100644 --- a/app/Models/Automation.php +++ b/app/Models/Automation.php @@ -4,6 +4,7 @@ namespace App\Models; +use App\Enums\Automation\Node\Type as NodeType; use App\Enums\Automation\Status; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -49,6 +50,7 @@ protected static function booted(): void $automation->variables ?? [], $automation->getOriginal('variables') ?? [], ); + $automation->trigger_type = self::deriveTriggerType($automation->nodes ?? []); }); } @@ -100,6 +102,20 @@ public function runs(): HasMany * never received the real value) so we keep the existing ciphertext. Plain * text values get encrypted; already-encrypted strings pass through. * + * Denormalize the trigger node's type into an indexed column so the + * scheduler can filter by it in SQL instead of decoding every automation's + * `nodes` JSON each minute. Recomputed on every save so it cannot drift. + * + * @param array> $nodes + */ + private static function deriveTriggerType(array $nodes): ?string + { + $triggerNode = collect($nodes)->firstWhere('type', NodeType::Trigger->value); + + return data_get($triggerNode, 'data.trigger_type'); + } + + /** * @param array> $incoming * @param array>|string $original * @return array> diff --git a/database/migrations/2026_05_22_211640_create_automations_table.php b/database/migrations/2026_05_22_211640_create_automations_table.php index 23f5d423..c7de2ea6 100644 --- a/database/migrations/2026_05_22_211640_create_automations_table.php +++ b/database/migrations/2026_05_22_211640_create_automations_table.php @@ -19,6 +19,7 @@ public function up(): void $table->foreignUuid('user_id')->nullable()->constrained('users')->nullOnDelete(); $table->string('name'); $table->string('status')->default('draft'); + $table->string('trigger_type')->nullable(); $table->json('nodes')->nullable(); $table->json('connections')->nullable(); $table->timestamp('activated_at')->nullable(); @@ -26,6 +27,7 @@ public function up(): void $table->timestamps(); $table->index(['workspace_id', 'status']); + $table->index(['status', 'trigger_type']); }); } diff --git a/tests/Feature/Automation/AutomationModelTest.php b/tests/Feature/Automation/AutomationModelTest.php index 437c1af1..4a83c620 100644 --- a/tests/Feature/Automation/AutomationModelTest.php +++ b/tests/Feature/Automation/AutomationModelTest.php @@ -17,6 +17,27 @@ expect($automation->nodes[0]['type'])->toBe('trigger'); }); +it('derives the trigger_type column from the trigger node on save', function () { + $automation = Automation::factory()->withScheduleTrigger()->create(); + + expect($automation->trigger_type)->toBe('schedule'); + + $automation->update([ + 'nodes' => [ + ['id' => 'trigger_1', 'type' => 'trigger', 'position' => ['x' => 0, 'y' => 0], + 'data' => ['trigger_type' => 'post_published']], + ], + ]); + + expect($automation->fresh()->trigger_type)->toBe('post_published'); +}); + +it('nulls the trigger_type column when there is no trigger node', function () { + $automation = Automation::factory()->create(['nodes' => []]); + + expect($automation->trigger_type)->toBeNull(); +}); + it('relates trigger items, runs and node runs', function () { $automation = Automation::factory()->create(); $item = AutomationTriggerItem::factory()->for($automation)->create(); diff --git a/tests/Feature/Automation/Command/FireScheduleTriggersTest.php b/tests/Feature/Automation/Command/FireScheduleTriggersTest.php index 7ce79e30..50dcf3f1 100644 --- a/tests/Feature/Automation/Command/FireScheduleTriggersTest.php +++ b/tests/Feature/Automation/Command/FireScheduleTriggersTest.php @@ -17,3 +17,14 @@ expect(AutomationTriggerItem::where('automation_id', $scheduleAutomation->id)->count())->toBe(1); expect(AutomationTriggerItem::where('automation_id', $rssAutomation->id)->count())->toBe(0); }); + +it('ignores automations whose trigger is not a schedule', function () { + $postAutomation = Automation::factory()->active()->create([ + 'nodes' => [['id' => 't', 'type' => 'trigger', 'position' => ['x' => 0, 'y' => 0], + 'data' => ['trigger_type' => 'post_published']]], + ]); + + $this->artisan('automation:fire-schedule')->assertSuccessful(); + + expect(AutomationTriggerItem::where('automation_id', $postAutomation->id)->count())->toBe(0); +});