diff --git a/appinfo/info.xml b/appinfo/info.xml
index 24a084875..a4a8df760 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -35,7 +35,7 @@ The OpenConnector Nextcloud app provides a ESB-framework to work together in an
-
+
diff --git a/src/views/Rule/RuleActionConfig.vue b/src/views/Rule/RuleActionConfig.vue
index 234ff8b78..6a5e10b98 100644
--- a/src/views/Rule/RuleActionConfig.vue
+++ b/src/views/Rule/RuleActionConfig.vue
@@ -2,24 +2,21 @@
@@ -39,61 +36,28 @@
-
-
-
-
-
- {{ t('openconnector', 'Retain original response') }}
-
-
- {{ t('openconnector', 'When enabled, the synchronization runs but the rule preserves the original response body instead of replacing it.') }}
-
+
+
+ patch('filenamePosition', next)" />
+
+
+ {{ t('openconnector', 'The endpoint checks if the authenticated user is allowed to read the resolved file before sending bytes back.') }}
+
+
+
+ patch('mappingId', option?.id ? String(option.id) : '')" />
+
+
+ patch('mappingOutId', option?.id ? String(option.id) : '')" />
+
+ {{ t('openconnector', 'Inbound runs before the part is written; outbound runs over the written object before it returns.') }}
+
+
+
+
+
+ {{ t('openconnector', 'The synchronization that runs when this rule matches. Pick by name; the underlying UUID is what gets stored.') }}
+
+
+ patch('retainResponse', !!next)">
+ {{ t('openconnector', 'Retain original response') }}
+
+
+ {{ t('openconnector', 'When enabled, the sync still runs but the rule preserves the original response body instead of replacing it.') }}
+
+
+ patch('objectIdPath', next)" />
+
+
+ patch('path', next)" />
+ patch('allowedTypes', next)" />
+
+
+ {{ t('openconnector', 'Note: the backend handler for upload is still pending. Configuration is persisted so it activates once the dispatcher case lands.') }}
+
+
+ patch('filePath', next)" />
+ patch('fileNamePath', next)" />
+ patch('tags', toArray(next))" />
+ patch('autoShare', !!next)">
+ {{ t('openconnector', 'Auto-share written files') }}
+
+
+ {{ t('openconnector', 'The dot path under the rule data tells the engine where to find the file bytes; the filename path supplies the name.') }}
+
+
+
+
+
+
+
diff --git a/src/views/Rule/actionForms/shared.js b/src/views/Rule/actionForms/shared.js
new file mode 100644
index 000000000..be612239b
--- /dev/null
+++ b/src/views/Rule/actionForms/shared.js
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: EUPL-1.2
+// Copyright (C) 2026 Conduction B.V.
+
+/**
+ * Shared helpers for the bespoke per-action-type form SFCs under
+ * src/views/Rule/actionForms. Each form receives a `value` prop (the
+ * slice of `configuration[]`) and emits `update:value` with the
+ * next slice; RuleActionConfig wraps that back into the full
+ * configuration blob.
+ *
+ * Picker helpers (`fetchOpenRegisterCollection`) follow the pattern
+ * established by RuleActionConfig.vue's `fetchSynchronizations` — load
+ * once on demand, no Vuex, no global cache.
+ */
+
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+
+/**
+ * Fetch a paginated list of OpenRegister objects for picker
+ * components. Tolerates both `{ results: [] }` and bare arrays.
+ *
+ * @param {string} schema OpenRegister schema slug (e.g. 'synchronization').
+ * @param {string} register Register slug (defaults to 'openconnector').
+ * @param {number} limit Max rows to load (default 500).
+ * @return {Promise>}
+ * Resolved options ready for NcSelect.
+ */
+export async function fetchOpenRegisterCollection(schema, register = 'openconnector', limit = 500) {
+ try {
+ const response = await axios.get(
+ generateUrl(`/apps/openregister/api/objects/${register}/${schema}`),
+ { params: { limit } },
+ )
+ const data = response.data
+ const list = Array.isArray(data?.results)
+ ? data.results
+ : Array.isArray(data)
+ ? data
+ : []
+ return list.map((row) => ({
+ id: String(row.id || row.uuid),
+ label: row.name || row.title || row.id || '(unnamed)',
+ raw: row,
+ }))
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.warn(`[actionForms] fetch ${register}/${schema} failed`, err)
+ return []
+ }
+}
+
+/**
+ * Mixin-like default behaviour: every action form uses the same
+ * `value` ↔ `update:value` contract, so we centralise the props
+ * declaration and the patch helper here. Components spread this into
+ * their `props` / `methods` to stay terse.
+ */
+export const valueProp = {
+ value: { type: Object, default: () => ({}) },
+}
+
+/**
+ * Patch-helper factory — returns a closure that, when called from a
+ * Vue component as `this.patch('field', newValue)`, emits an updated
+ * copy of the form's `value` with that key/value pair applied.
+ *
+ * The returned function takes:
+ * - key {string} Field name within the action's config slot.
+ * - next {*} New value to write at that key.
+ *
+ * @return {Function} The patch closure to be installed on a component.
+ */
+export function patchMethod() {
+ return function patch(key, next) {
+ const base = (this.value && typeof this.value === 'object') ? this.value : {}
+ const merged = { ...base, [key]: next }
+ this.$emit('update:value', merged)
+ }
+}