# Add webhook / event support for Discourse AI artifact key-value updates (or allow admins to disable sandboxing)

**URL:** https://meta.discourse.org/t/add-webhook-event-support-for-discourse-ai-artifact-key-value-updates-or-allow-admins-to-disable-sandboxing/403865
**Category:** Feature
**Tags:** webhooks, ai, ai-artifacts
**Created:** [May 26, 2026, 6:32pm UTC](https://meta.discourse.org/t/add-webhook-event-support-for-discourse-ai-artifact-key-value-updates-or-allow-admins-to-disable-sandboxing/403865 "2026-05-26T18:32:38Z")
**Posts on this page:** 5
**Page:** 1

<div class="post-metadata">

### Author: ![MachineScholar](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/machinescholar/32/424719_2.png) [@MachineScholar](https://meta.discourse.org/u/MachineScholar)
#### Post date: [May 26, 2026, 6:32pm UTC](https://meta.discourse.org/t/add-webhook-event-support-for-discourse-ai-artifact-key-value-updates-or-allow-admins-to-disable-sandboxing/403865/1 "2026-05-26T18:32:38Z")

</div>

I’d like to propose allowing AI artifacts to send webhooks whenever the stored data in an AI artifact is updated. There are many reasons why this would be advantageous, but I’ll try to keep my case short.

Many organization who make use of Discourse also make use of various automations, and the tools for automations are becoming increasingly popular, easy to set up, and beginner friendly. AI artifacts can contain precious data which can be used very effectively in such automations with the added bonus that it’s already JSON!

In my case, I use n8n in a Docker Compose setup next to my Discourse container and I already use it to automate a wide variety of things, including things on my Discourse instances via a Docker network. However, one of the Discourse instances I maintain is for an educational organization/business and AI artifacts are used to track learner notes, lesson journals, and so on. This kind of data would be really beneficial for improving teachers’ workflows, making summaries, and so on via an automation tool such as n8n.

It would be technically possible to poll artifacts for updates, but that could easily begin to start straining system resources and it would be a waste of resources at best, as well as require extensive technical skills to set up which many admins don’t have.

**Needless to say, this would also be highly beneficial for Discourse’s enterprise clients.**

A workaround might be for the artifact’s JavaScript to call an external webhook after calling `window.discourseArtifact.set(...)`.

However, this would have some limitations:

1. It is browser-side and therefore not authoritative.

2. It only fires if the artifact JavaScript executes successfully in the user’s browser.

3. It can be affected by CORS, browser privacy tools, network blockers, or sandbox constraints.

4. The payload cannot be trusted as coming from Discourse unless additional server-side verification is built.

5. Secrets cannot be safely embedded in artifact JavaScript.

A server-side event/webhook would be much more reliable and secure.

> **I'm not a Ruby developer, so I have been chatting with GPT-5.5 about this and it has offered some interesting insight as well…**
>
> Based on Discourse’s current webhook architecture, the clean implementation is probably **not** a bespoke “call this URL” feature inside AI artifacts. It should be implemented as a normal Discourse webhook event type, backed by a normal internal `DiscourseEvent`.
> 
> ## Best implementation shape
> 
> I’d suggest the Discourse devs implement it in two layers:
> 
> 1. **Internal events** emitted when artifact KV records change.
> 
> 2. **Webhook event types** that subscribe to those internal events and use the existing webhook delivery system.
> 
> That keeps it consistent with the rest of Discourse. Existing webhooks already work by mapping internal `DiscourseEvent`s to `WebHook.enqueue_*` calls in `config/initializers/012-web_hook_events.rb`; for example, topic, post, user, category, tag, reviewable, notification, and like events are wired this way.
> 
> The artifact storage path is simple enough: `ArtifactKeyValuesController#set` finds or initializes a key-value record, assigns `key/value/public`, and saves it; `destroy` finds the current user’s record by key and destroys it. The model itself is `AiArtifactKeyValue`, belongs to an artifact and a user, has `key`, `value`, and `public`, and enforces uniqueness by artifact/user/key.
> 
> ## Proposed event names
> 
> I’d probably use three specific webhook events:
> 
> ```ruby
> ai_artifact_key_value_created
> ai_artifact_key_value_updated
> ai_artifact_key_value_deleted
> 
> ```
> 
> Alternatively, a single event could work:
> 
> ```ruby
> ai_artifact_key_value_changed
> 
> ```
> 
> …but three events fit Discourse’s existing style better. Discourse already has separate webhook event names like `post_created`, `post_edited`, `post_destroyed`, `calendar_event_created`, `calendar_event_updated`, and so on.
> 
> ## Files/classes they would likely touch
> 
> ### 1. Add new webhook event types
> 
> `WebHookEventType` currently defines numeric constants, a `group` enum, and a `TYPES` hash of event names to IDs.
> 
> They could add something like:
> 
> ```ruby
> AI_ARTIFACT = 20
> 
> enum :group,
> {
> # existing groups...
> ai_artifact: 18,
> },
> scopes: false
> 
> TYPES = {
> # existing types...
> ai_artifact_key_value_created: 2001,
> ai_artifact_key_value_updated: 2002,
> ai_artifact_key_value_deleted: 2003,
> }
> 
> ```
> 
> The exact IDs would be up to Discourse maintainers; they just need to avoid conflicts.
> 
> They would also add seed entries in `db/fixtures/007_web_hook_event_types.rb`, because existing webhook event types are seeded there with IDs, names, and groups.
> 
> Example:
> 
> ```ruby
> WebHookEventType.seed do |b|
> b.id = WebHookEventType::TYPES[:ai_artifact_key_value_created]
> b.name = "ai_artifact_key_value_created"
> b.group = WebHookEventType.groups[:ai_artifact]
> end
> 
> WebHookEventType.seed do |b|
> b.id = WebHookEventType::TYPES[:ai_artifact_key_value_updated]
> b.name = "ai_artifact_key_value_updated"
> b.group = WebHookEventType.groups[:ai_artifact]
> end
> 
> WebHookEventType.seed do |b|
> b.id = WebHookEventType::TYPES[:ai_artifact_key_value_deleted]
> b.name = "ai_artifact_key_value_deleted"
> b.group = WebHookEventType.groups[:ai_artifact]
> end
> 
> ```
> 
> The admin webhook UI should then pick these up automatically, because `Admin::WebHooksController#index` already serializes grouped active event types for the UI. The event type serializer already exposes `id`, `name`, and `group`.
> 
> ### 2. Hide the events when Discourse AI is disabled
> 
> `WebHookEventType.active` already hides plugin-dependent webhook events when their features are disabled, such as solved, assign, topic voting, chat, and calendar events.
> 
> So they could add:
> 
> ```ruby
> unless defined?(SiteSetting.discourse_ai_enabled) && SiteSetting.discourse_ai_enabled
> ids_to_exclude.concat(
> [
> TYPES[:ai_artifact_key_value_created],
> TYPES[:ai_artifact_key_value_updated],
> TYPES[:ai_artifact_key_value_deleted],
> ],
> )
> end
> 
> ```
> 
> They may also want to check an artifact-specific setting if one exists or is added.
> 
> ### 3. Emit internal events from the model, not just the controller
> 
> Although the current write path is controller-based, the robust place is probably the model, using commit callbacks:
> 
> ```ruby
> class AiArtifactKeyValue < ActiveRecord::Base
> after_create_commit :trigger_created_event
> after_update_commit :trigger_updated_event
> after_destroy_commit :trigger_deleted_event
> 
> private
> 
> def trigger_created_event
> DiscourseEvent.trigger(:ai_artifact_key_value_created, self)
> end
> 
> def trigger_updated_event
> return if previous_changes.slice("value", "public").blank?
> 
> DiscourseEvent.trigger(:ai_artifact_key_value_updated, self)
> end
> 
> def trigger_deleted_event
> DiscourseEvent.trigger(:ai_artifact_key_value_deleted, self)
> end
> end
> 
> ```
> 
> Model-level callbacks would catch future write paths too, not just `ArtifactKeyValuesController#set`. The `after_commit` part matters because webhook delivery should only be queued after the database change is safely committed.
> 
> One subtlety: they probably should **not** trigger an “updated” event when the artifact calls `set()` with the same value and nothing actually changes. Checking `previous_changes` avoids noisy webhooks.
> 
> ### 4. Add a webhook payload serializer
> 
> Discourse webhooks generate payloads through serializers. The generic `WebHook.enqueue_object_hooks` method can take a serializer, and `WebHook.generate_payload` serializes the object with a system-user guardian.
> 
> They could add something like:
> 
> ```ruby
> class WebHookAiArtifactKeyValueSerializer < ApplicationSerializer
> attributes :id,
> :ai_artifact_id,
> :post_id,
> :topic_id,
> :user_id,
> :key,
> :public,
> :value_included,
> :created_at,
> :updated_at
> 
> def post_id
> object.ai_artifact.post_id
> end
> 
> def topic_id
> object.ai_artifact.post.topic_id
> end
> 
> def value_included
> false
> end
> end
> 
> ```
> 
> I’d strongly recommend **not including `value` by default**. Artifact KV data can be private per user, and the model has a `public` flag. If Discourse wants to support values, they could make it explicit:
> 
> ```ruby
> include_value: false
> include_public_values: true
> include_private_values: false
> 
> ```
> 
> But the safe v1 should probably omit values entirely.
> 
> ### 5. Wire internal events to webhook delivery
> 
> They could add handlers to `config/initializers/012-web_hook_events.rb`, mirroring existing patterns.
> 
> Something like:
> 
> ```ruby
> %i[
> ai_artifact_key_value_created
> ai_artifact_key_value_updated
> ai_artifact_key_value_deleted
> ].each do |event|
> DiscourseEvent.on(event) do |key_value|
> artifact = key_value.ai_artifact
> post = artifact.post
> topic = post.topic
> 
> payload =
> WebHook.generate_payload(
> :ai_artifact_key_value,
> key_value,
> WebHookAiArtifactKeyValueSerializer
> )
> 
> WebHook.enqueue_hooks(
> :ai_artifact_key_value,
> event,
> id: key_value.id,
> category_id: topic&.category_id,
> tag_ids: topic&.tags&.pluck(:id),
> payload: payload
> )
> end
> end
> 
> ```
> 
> That would reuse the existing webhook job pipeline. `WebHook.enqueue_hooks` finds active webhooks for the event and enqueues `Jobs::EmitWebHookEvent`. The job already handles delivery, retries, logging, headers, signatures, and admin visibility.
> 
> Including `category_id` and `tag_ids` is a nice touch because existing webhook jobs can filter by category and tag. The webhook job already checks category and tag constraints before sending. Since an AI artifact belongs to a post, and the artifact belongs to a post in the model, deriving category/topic context should be possible.
> 
> ## What the delivered webhook might look like
> 
> Because Discourse’s webhook body uses `event_type` as the top-level root, this would probably look like:
> 
> ```json
> {
> "ai_artifact_key_value": {
> "id": 456,
> "ai_artifact_id": 123,
> "post_id": 789,
> "topic_id": 321,
> "user_id": 42,
> "key": "score",
> "public": true,
> "value_included": false,
> "created_at": "2026-05-26T12:00:00Z",
> "updated_at": "2026-05-26T12:05:00Z"
> }
> }
> 
> ```
> 
> The event name would be in the headers, consistent with existing webhook delivery:
> 
> ```plaintext
> X-Discourse-Event-Type: ai_artifact_key_value
> X-Discourse-Event: ai_artifact_key_value_updated
> X-Discourse-Event-Signature: sha256=...
> 
> ```
> 
> Those headers line up with how `EmitWebHookEvent` builds webhook headers today.
> 
> ## The devs would probably need to decide these:
> 
> **Should values be included?**  
> My vote: no by default. Maybe allow only public values, or make value inclusion an admin setting. Private per-user artifact data should not silently leave the site.
> 
> **Should there be one event or three?**  
> Three is cleaner for webhook subscribers. One event with an `action` field is simpler internally. Existing Discourse style leans toward multiple event names.
> 
> **Should category/tag filtering apply?**  
> I think yes. The artifact is attached to a post/topic, so category and tag filters are meaningful.
> 
> **Should this be implemented in Discourse core or the Discourse AI plugin?**  
> The data model lives in the Discourse AI plugin, but the webhook event registry lives in core. Since bundled plugins like solved/chat/calendar already have webhook event types in the shared `WebHookEventType` list, Discourse may be comfortable adding AI artifact events there too. The `active` method can hide them when AI is disabled.
> 
> **Should this also emit an internal `DiscourseEvent` even if no webhook exists?**  
> Yes. That gives plugin authors a clean hook point even apart from webhooks.
> 
> ## Complexity
> 
> I’d frame it as **moderate but very contained**.
> 
> The likely work is:
> 
> 1. Add 3 event type constants.
> 
> 2. Add 1 webhook group.
> 
> 3. Add 3 seed records.
> 
> 4. Add 1 serializer.
> 
> 5. Add 3 model callbacks or service-level event triggers.
> 
> 6. Add webhook initializer handlers.
> 
> 7. Add specs for create/update/delete.
> 
> 8. Add privacy decision around whether `value` is included.
> 
> The delivery system, retries, signatures, event log, ping/redelivery UI, and admin webhook configuration are already there. The feature is mostly about making artifact KV mutations visible to that existing system.

---

<div class="post-metadata">

### Author: ![Falco](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/falco/32/179432_2.png) [@Falco](https://meta.discourse.org/u/Falco)
#### Post date: [May 26, 2026, 6:34pm UTC](https://meta.discourse.org/t/add-webhook-event-support-for-discourse-ai-artifact-key-value-updates-or-allow-admins-to-disable-sandboxing/403865/2 "2026-05-26T18:34:58Z")

</div>

Something to revisit once we land Workflows, the new approach for Automation in Discourse.

---

<div class="post-metadata">

### Author: ![MachineScholar](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/machinescholar/32/424719_2.png) [@MachineScholar](https://meta.discourse.org/u/MachineScholar)
#### Post date: [May 26, 2026, 6:37pm UTC](https://meta.discourse.org/t/add-webhook-event-support-for-discourse-ai-artifact-key-value-updates-or-allow-admins-to-disable-sandboxing/403865/3 "2026-05-26T18:37:38Z")

</div>

Ooooh that sounds quite intriguing indeed. Have I missed any information about that here on Meta and is there any timeline?

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [May 27, 2026, 4:36am UTC](https://meta.discourse.org/t/add-webhook-event-support-for-discourse-ai-artifact-key-value-updates-or-allow-admins-to-disable-sandboxing/403865/4 "2026-05-27T04:36:38Z")

</div>

I still do like the suggestion of adding a server side webhook especially if it can be scope on artifact id

Also acts as advertising for the power of artifact data storage

---

<div class="post-metadata">

### Author: ![MachineScholar](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/machinescholar/32/424719_2.png) [@MachineScholar](https://meta.discourse.org/u/MachineScholar)
#### Post date: [September 9, 2026, 9:42pm UTC](https://meta.discourse.org/t/add-webhook-event-support-for-discourse-ai-artifact-key-value-updates-or-allow-admins-to-disable-sandboxing/403865/5 "2026-09-09T21:42:06Z")

</div>

> [@Falco](#):
>
> Something to revisit once we land Workflows, the new approach for Automation in Discourse

May I ask if there have been any updates on a potential implementation of this feature? 😅
