Migration issue with workflows

Hi I don’t have Workflows enabled but it appears to be filling my log with entries.

Discourse | v2026.7.0-latest +394

Job exception: PG::UndefinedTable: ERROR: relation “discourse_workflows_webhooks” does not exist LINE 10: WHERE a.attrelid = ‘“discourse_workflows_webhooks”’::regcla…

Message (252 copies reported)

Job exception: PG::UndefinedTable: ERROR: relation “discourse_workflows_webhooks” does not exist
LINE 10: WHERE a.attrelid = ‘“discourse_workflows_webhooks”’::regcla…

^

Backtrace

rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in 'PG::Connection#exec'
rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in 'PG::Connection#async_exec'
activerecord-8.0.5/lib/active_record/connection_adapters/postgresql/database_statements.rb:167:in 'ActiveRecord::ConnectionAdapters::PostgreSQL::DatabaseStatements#perform_query'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/database_statements.rb:556:in 'block (2 levels) in ActiveRecord::ConnectionAdapters::DatabaseStatements#raw_execute'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract_adapter.rb:1022:in 'block in ActiveRecord::ConnectionAdapters::AbstractAdapter#with_raw_connection'
activesupport-8.0.5/lib/active_support/concurrency/null_lock.rb:9:in 'ActiveSupport::Concurrency::NullLock#synchronize'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract_adapter.rb:991:in 'ActiveRecord::ConnectionAdapters::AbstractAdapter#with_raw_connection'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/database_statements.rb:555:in 'block in ActiveRecord::ConnectionAdapters::DatabaseStatements#raw_execute'
activesupport-8.0.5/lib/active_support/notifications/instrumenter.rb:58:in 'ActiveSupport::Notifications::Instrumenter#instrument'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract_adapter.rb:1142:in 'ActiveRecord::ConnectionAdapters::AbstractAdapter#log'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/database_statements.rb:554:in 'ActiveRecord::ConnectionAdapters::DatabaseStatements#raw_execute'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/database_statements.rb:591:in 'ActiveRecord::ConnectionAdapters::DatabaseStatements#internal_execute'
activerecord-8.0.5/lib/active_record/connection_adapters/postgresql/database_statements.rb:15:in 'ActiveRecord::ConnectionAdapters::PostgreSQL::DatabaseStatements#query'
activerecord-8.0.5/lib/active_record/connection_adapters/postgresql_adapter.rb:1042:in 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#column_definitions'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/schema_statements.rb:109:in 'ActiveRecord::ConnectionAdapters::SchemaStatements#columns'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:345:in 'block (2 levels) in ActiveRecord::ConnectionAdapters::SchemaCache#columns'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/connection_pool.rb:416:in 'ActiveRecord::ConnectionAdapters::ConnectionPool#with_connection'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:344:in 'block in ActiveRecord::ConnectionAdapters::SchemaCache#columns'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:343:in 'Hash#fetch'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:343:in 'ActiveRecord::ConnectionAdapters::SchemaCache#columns'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:354:in 'block in ActiveRecord::ConnectionAdapters::SchemaCache#columns_hash'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:353:in 'Hash#fetch'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:353:in 'ActiveRecord::ConnectionAdapters::SchemaCache#columns_hash'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:54:in 'ActiveRecord::ConnectionAdapters::SchemaReflection#columns_hash'
activerecord-8.0.5/lib/active_record/connection_adapters/schema_cache.rb:198:in 'ActiveRecord::ConnectionAdapters::BoundSchemaReflection#columns_hash'
activerecord-8.0.5/lib/active_record/model_schema.rb:592:in 'ActiveRecord::ModelSchema::ClassMethods#load_schema!'
activerecord-8.0.5/lib/active_record/counter_cache.rb:187:in 'ActiveRecord::CounterCache::ClassMethods#load_schema!'
activerecord-8.0.5/lib/active_record/encryption/encryptable_record.rb:127:in 'ActiveRecord::Encryption::EncryptableRecord::ClassMethods#load_schema!'
activerecord-8.0.5/lib/active_record/model_schema.rb:539:in 'block in ActiveRecord::ModelSchema::ClassMethods#load_schema'
activerecord-8.0.5/lib/active_record/model_schema.rb:536:in 'Monitor#synchronize'
activerecord-8.0.5/lib/active_record/model_schema.rb:536:in 'ActiveRecord::ModelSchema::ClassMethods#load_schema'
activerecord-8.0.5/lib/active_record/model_schema.rb:428:in 'ActiveRecord::ModelSchema::ClassMethods#columns_hash'
activerecord-8.0.5/lib/active_record/attributes.rb:255:in 'block in ActiveRecord::Attributes::ClassMethods#_default_attributes'
activerecord-8.0.5/lib/active_record/connection_adapters/abstract/connection_pool.rb:422:in 'ActiveRecord::ConnectionAdapters::ConnectionPool#with_connection'
activerecord-8.0.5/lib/active_record/connection_handling.rb:312:in 'ActiveRecord::ConnectionHandling#with_connection'
activerecord-8.0.5/lib/active_record/attributes.rb:254:in 'ActiveRecord::Attributes::ClassMethods#_default_attributes'
activemodel-8.0.5/lib/active_model/attribute_registration.rb:38:in 'ActiveModel::AttributeRegistration::ClassMethods#attribute_types'
activemodel-8.0.5/lib/active_model/attribute_registration.rb:49:in 'ActiveModel::AttributeRegistration::ClassMethods#type_for_attribute'
activerecord-8.0.5/lib/active_record/type_caster/map.rb:16:in 'ActiveRecord::TypeCaster::Map#type_for_attribute'
activerecord-8.0.5/lib/arel/table.rb:107:in 'Arel::Table#type_for_attribute'
activerecord-8.0.5/lib/active_record/table_metadata.rb:18:in 'ActiveRecord::TableMetadata#type'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:59:in 'ActiveRecord::PredicateBuilder#build'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:54:in 'ActiveRecord::PredicateBuilder#
'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:145:in 'block in ActiveRecord::PredicateBuilder#expand_from_hash'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:87:in 'Hash#each'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:87:in 'Enumerable#flat_map'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:87:in 'ActiveRecord::PredicateBuilder#expand_from_hash'
activerecord-8.0.5/lib/active_record/relation/predicate_builder.rb:25:in 'ActiveRecord::PredicateBuilder#build_from_hash'
activerecord-8.0.5/lib/active_record/relation/query_methods.rb:1643:in 'ActiveRecord::QueryMethods#build_where_clause'
activerecord-8.0.5/lib/active_record/relation/query_methods.rb:1044:in 'ActiveRecord::QueryMethods#where!'
activerecord-8.0.5/lib/active_record/relation/query_methods.rb:1039:in 'ActiveRecord::QueryMethods#where'
/var/www/discourse/plugins/discourse-workflows/app/models/discourse_workflows/webhook.rb:12:in 'block in class:Webhook'
activerecord-8.0.5/lib/active_record/relation.rb:565:in 'BasicObject#instance_exec'
activerecord-8.0.5/lib/active_record/relation.rb:565:in 'block in ActiveRecord::Relation#_exec_scope'
activerecord-8.0.5/lib/active_record/relation.rb:1383:in 'ActiveRecord::Relation#_scoping'
activerecord-8.0.5/lib/active_record/relation.rb:565:in 'ActiveRecord::Relation#_exec_scope'
activerecord-8.0.5/lib/active_record/scoping/named.rb:175:in 'block in DiscourseWorkflows::Webhook.scope'
/var/www/discourse/plugins/discourse-workflows/lib/discourse_workflows/webhook_test_listener.rb:88:in 'DiscourseWorkflows::WebhookTestListener.purge_expired!'
/var/www/discourse/plugins/discourse-workflows/app/jobs/scheduled/discourse_workflows/purge_expired_webhook_test_listeners.rb:9:in 'Jobs::DiscourseWorkflows::PurgeExpiredWebhookTestListeners#execute'
/var/www/discourse/app/jobs/base.rb:313:in 'block (2 levels) in Jobs::Base#perform'
rails_multisite-7.0.0/lib/rails_multisite/connection_management/null_instance.rb:49:in 'RailsMultisite::ConnectionManagement::NullInstance#with_connection'
rails_multisite-7.0.0/lib/rails_multisite/connection_management.rb:17:in 'RailsMultisite::ConnectionManagement.with_connection'
/var/www/discourse/app/jobs/base.rb:300:in 'block in Jobs::Base#perform'
/var/www/discourse/app/jobs/base.rb:297:in 'Array#each'
/var/www/discourse/app/jobs/base.rb:297:in 'Jobs::Base#perform'
/var/www/discourse/app/jobs/base.rb:375:in 'Jobs::Scheduled#perform'
mini_scheduler-0.20.0/lib/mini_scheduler/manager.rb:137:in 'MiniScheduler::Manager::Runner#process_queue'
mini_scheduler-0.20.0/lib/mini_scheduler/manager.rb:77:in 'MiniScheduler::Manager::Runner#worker_loop'
mini_scheduler-0.20.0/lib/mini_scheduler/manager.rb:63:in 'block (2 levels) in MiniScheduler::Manager::Runner#ensure_worker_threads'




It looks like you have the plugin installed, but haven’t run migrations to create the necessary tables it uses.

Can you show us the part of your container definition where you install it?

Thanks, but discourse-workflows isn’t something I installed; it’s a bundled core plugin (ships in plugins/, authored by Discourse, behind enable_discourse_workflows), so there’s nothing in my app.yml for it.

The the table has a migration line 212 of db/migrate/20260312000000_create_workflow_tables.rb which creates discourse_workflows_webhooks but it never ran on my database.

Posting mainly in case it’s worth a guard on that scheduled job (PurgeExpiredWebhookTestListeners) so a missing table degrades quietly instead of erroring on every run.

1 Like

The table discourse_workflows_webhooks does have a migration that creates it (create_workflow_tables, version 20260312000000, the create_table sits near the bottom of that file).

On my database that migration is already recorded as run in schema_migrations, but the webhooks table itself was never created.

A clean db:migrate confirmed it: it ran with no output, so nothing was pending, yet the table still did not exist. Rails keys off the version number, sees the migration as done, and never runs it again, so the table stays missing while the model and the scheduled purge job keep loading and firing. That is why the same job exception kept piling up.

The fix I applied: Since the migration is recorded, running it again was not an option, because it would try to recreate the other workflow tables that already exist and fail.

So I created the missing table directly, matching the migration exactly (same columns, same five indexes including the unique one):

conn = ActiveRecord::Base.connection
unless conn.table_exists?(:discourse_workflows_webhooks)
conn.create_table :discourse_workflows_webhooks do |t|
t.bigint :workflow_id, null: false
t.string :workflow_version_id, limit: 36
t.string :node_name, null: false, limit: 100
t.string :webhook_path, null: false, limit: 500
t.string :http_method, null: false, limit: 10
t.string :webhook_id, limit: 36
t.integer :path_length
t.boolean :test_webhook, null: false, default: false
t.integer :user_id
t.jsonb :workflow_snapshot
t.datetime :expires_at
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
end
conn.add_index :discourse_workflows_webhooks, %i[http_method webhook_path test_webhook], unique: true, name: "idx_dwf_webhooks_on_method_path_test"
conn.add_index :discourse_workflows_webhooks, %i[webhook_id http_method test_webhook], name: "idx_dwf_webhooks_on_webhook_id_method_test", where: "webhook_id IS NOT NULL"
conn.add_index :discourse_workflows_webhooks, :workflow_id, name: "idx_dwf_webhooks_on_workflow_id"
conn.add_index :discourse_workflows_webhooks, :workflow_version_id, name: "idx_dwf_webhooks_on_workflow_version_id"
conn.add_index :discourse_workflows_webhooks, :expires_at, name: "idx_dwf_webhooks_on_expires_at", where: "expires_at IS NOT NULL"
end

After that, table_exists? returns true and the PurgeExpiredWebhookTestListeners job stops erroring on its next run.

Because this came from a shipped migration being edited after it had already run, a later core update that adds a proper migration for this table could try to create it again and fail on “already exists”.

If that shows up after an update, the answer is to let the new migration win: drop the table I created by hand first, or insert its version into schema_migrations so it gets skipped.

Flagging it in case anyone else on the latest branch hits the same job exception.

3 Likes