Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
92b0993
PoC for webhook responses
midigofrank Apr 22, 2026
53b09e6
Allow users to change status codes from UI
midigofrank Apr 22, 2026
5dd0bfe
add fields to snapshot
midigofrank Apr 22, 2026
54535d1
use jsonb config
midigofrank Apr 24, 2026
2f817e6
refactor webhook config to allow for error and success status
midigofrank Apr 24, 2026
e356ad1
add tests and fix bugs for configurable webhook responses
midigofrank Apr 27, 2026
e170250
handle webhook_response in YAML export, provisioning API, and import
midigofrank Apr 27, 2026
9d82ae9
handle webhook_response in YAML code view and unsaved changes detection
midigofrank Apr 27, 2026
dd55b65
Give an example on how _webhookResponse key will look like
midigofrank Apr 27, 2026
a66a35b
fix failing tests
midigofrank Apr 27, 2026
8c90813
remove body from configuration
midigofrank Apr 28, 2026
de52d62
remove left over code
midigofrank Apr 28, 2026
9f357d1
fix failing tests
midigofrank Apr 28, 2026
1294869
remove leftover body references in the provisioner
midigofrank Apr 29, 2026
8f23d20
feat: support undo/redo
doc-han May 5, 2026
6ce963f
Merge branch 'main' of github.com:OpenFn/lightning into configurable-…
midigofrank May 8, 2026
48215b7
Source webhook_response from step:complete instead of run:complete fi…
midigofrank May 8, 2026
15793d8
Fix after-completion webhook response for manual runs and success-wit…
midigofrank May 11, 2026
efcf39c
update old references of _webhookResponse
midigofrank May 11, 2026
09cbccd
Merge branch 'main' of github.com:OpenFn/lightning into configurable-…
midigofrank May 11, 2026
18aaa09
return 500 on error/crash by default
midigofrank May 11, 2026
78b5f42
update changelog
midigofrank May 11, 2026
a6350ea
credo
midigofrank May 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ and this project adheres to

### Added

- Allow users to respond back with custom webhook responses via the
`webhookResponse` field in the job state.
[#3102](https://github.com/OpenFn/lightning/issues/3102)

### Changed

### Fixed

## [2.16.3] - 2026-05-07

## [2.16.3-pre3] - 2026-05-07

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions assets/js/collaborative-editor/adapters/YAMLStateToYDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export class YAMLStateToYDoc {

if (trigger.type === 'webhook') {
triggerMap.set('webhook_reply', trigger.webhook_reply ?? null);
triggerMap.set(
'sync_webhook_response_config',
trigger.sync_webhook_response_config ?? null
);
}

return triggerMap;
Expand Down
332 changes: 329 additions & 3 deletions assets/js/collaborative-editor/components/inspector/TriggerForm.tsx

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions assets/js/collaborative-editor/hooks/useUnsavedChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ function transformTrigger(trigger: Trigger) {
break;
case 'webhook':
output.webhook_reply = trigger.webhook_reply ?? 'before_start';
output.sync_webhook_response_config =
trigger.sync_webhook_response_config ?? null;
break;
}
return output;
Expand Down
4 changes: 4 additions & 0 deletions assets/js/collaborative-editor/types/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ export namespace Session {
cron_expression: string | null;
has_auth_method: boolean;
webhook_reply: 'before_start' | 'after_completion' | null;
sync_webhook_response_config: {
success_code: number | null;
error_code: number | null;
} | null;
webhook_auth_methods: Array<{
id: string;
name: string;
Expand Down
10 changes: 10 additions & 0 deletions assets/js/collaborative-editor/types/trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ const webhookTriggerSchema = baseTriggerSchema.extend({
.enum(['before_start', 'after_completion'])
.nullable()
.default('before_start'),
sync_webhook_response_config: z
.object({
success_code: z.number().int().nullable().default(null),
error_code: z.number().int().nullable().default(null),
})
.nullable()
.default(null),
});

// Cron trigger schema with professional validation using cron-validator
Expand All @@ -49,6 +56,7 @@ const cronTriggerSchema = baseTriggerSchema.extend({
),
cron_cursor_job_id: z.string().uuid().nullable().default(null),
kafka_configuration: z.null().default(null),
sync_webhook_response_config: z.null().default(null),
webhook_reply: z.null().default(null).catch(null),
});

Expand Down Expand Up @@ -103,6 +111,7 @@ const kafkaTriggerSchema = baseTriggerSchema.extend({
cron_expression: z.null().default(null),
cron_cursor_job_id: z.null().default(null),
kafka_configuration: kafkaConfigSchema,
sync_webhook_response_config: z.null().default(null),
webhook_reply: z.null().default(null).catch(null),
});

Expand Down Expand Up @@ -137,6 +146,7 @@ export const createDefaultTrigger = (
cron_cursor_job_id: null,
kafka_configuration: null,
webhook_reply: 'before_start' as const,
sync_webhook_response_config: null,
};

case 'cron':
Expand Down
8 changes: 8 additions & 0 deletions assets/js/yaml/schema/workflow-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@
"type": ["string", "null"],
"enum": ["before_start", "after_completion", "custom", null]
},
"webhook_response": {
"type": ["object", "null"],
"properties": {
"success_code": { "type": ["integer", "null"] },
"error_code": { "type": ["integer", "null"] }
},
"additionalProperties": false
},
"pos": {
"type": "object",
"properties": {
Expand Down
12 changes: 11 additions & 1 deletion assets/js/yaml/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ export type StateWebhookTrigger = {
id: string;
enabled: boolean;
type: 'webhook';
webhook_reply: 'before_start' | 'after_completion' | 'custom' | null;
webhook_reply: 'before_start' | 'after_completion' | null | undefined;
sync_webhook_response_config?: {
success_code?: number | null;
error_code?: number | null;
} | null;
};

export type StateKafkaTrigger = {
Expand Down Expand Up @@ -87,11 +91,17 @@ export type SpecCronTrigger = {
pos: Position | undefined;
};

export type WebhookResponseConfig = {
success_code?: number | null;
error_code?: number | null;
};

export type SpecWebhookTrigger = {
id?: string;
type: 'webhook';
enabled: boolean;
webhook_reply: string | null;
webhook_response?: WebhookResponseConfig | null;
pos: Position | undefined;
};

Expand Down
22 changes: 22 additions & 0 deletions assets/js/yaml/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ export const convertWorkflowStateToSpec = (

if (trigger.type === 'webhook') {
triggerDetails.webhook_reply = trigger.webhook_reply ?? null;
const config = trigger.sync_webhook_response_config;
if (
config &&
(config.success_code != null || config.error_code != null)
) {
triggerDetails.webhook_response = {
...(config.success_code != null && {
success_code: config.success_code,
}),
...(config.error_code != null && { error_code: config.error_code }),
};
}
}

// TODO: handle kafka config
Expand Down Expand Up @@ -186,6 +198,9 @@ export const convertWorkflowSpecToState = (
type: 'webhook',
enabled,
webhook_reply: specTrigger.webhook_reply,
sync_webhook_response_config: parseWebhookResponseConfig(
specTrigger.webhook_response
),
};
} else {
trigger = {
Expand Down Expand Up @@ -251,6 +266,13 @@ export const convertWorkflowSpecToState = (
return workflowState;
};

function parseWebhookResponseConfig(
config: import('./types').WebhookResponseConfig | null | undefined
): import('./types').WebhookResponseConfig | null {
if (!config) return null;
return config;
}

export const extractJobCredentials = (jobs: Workflow.Job[]): JobCredentials => {
const credentials: JobCredentials = {};
for (const job of jobs) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,11 @@ describe('TriggerForm - Response Mode Field', () => {

// Check both options exist (use exact text to avoid "Async" matching "sync")
expect(
screen.getByRole('option', { name: 'Async (default)' })
screen.getByRole('option', { name: 'Async (Before Start)' })
).toBeInTheDocument();
expect(
screen.getByRole('option', { name: 'Sync (After Completion)' })
).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'Sync' })).toBeInTheDocument();
});

test('shows Async as default selected value', async () => {
Expand Down Expand Up @@ -261,7 +263,7 @@ describe('TriggerForm - Response Mode Field', () => {
await waitFor(() => {
expect(
screen.getByText(
/responds with the final output state after the run completes/i
/holds the http connection open and responds when the run completes/i
)
).toBeInTheDocument();
});
Expand Down
38 changes: 36 additions & 2 deletions lib/lightning/collaboration/workflow_serializer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,18 @@ defmodule Lightning.Collaboration.WorkflowSerializer do
"id" => trigger.id,
"type" => trigger.type |> to_string(),
"webhook_reply" =>
trigger.webhook_reply && to_string(trigger.webhook_reply)
trigger.webhook_reply && to_string(trigger.webhook_reply),
"sync_webhook_response_config" =>
case trigger.sync_webhook_response_config do
nil ->
nil

config ->
Yex.MapPrelim.from(%{
"success_code" => config.success_code,
"error_code" => config.error_code
})
end
})

Yex.Array.push(triggers_array, trigger_map)
Expand Down Expand Up @@ -274,9 +285,11 @@ defmodule Lightning.Collaboration.WorkflowSerializer do
|> Enum.map(fn trigger ->
trigger
|> Map.take(
~w(id type enabled cron_expression cron_cursor_job_id webhook_reply kafka_configuration)
~w(id type enabled cron_expression cron_cursor_job_id webhook_reply
kafka_configuration sync_webhook_response_config)
)
|> normalize_kafka_configuration()
|> normalize_sync_webhook_response_config()
end)
end

Expand All @@ -299,6 +312,27 @@ defmodule Lightning.Collaboration.WorkflowSerializer do

defp normalize_kafka_configuration(trigger), do: trigger

# Y.Doc serialises numbers as floats; convert integer codes back.
defp normalize_sync_webhook_response_config(
%{"sync_webhook_response_config" => %{} = config} = trigger
) do
normalized =
config
|> normalize_integer_field("success_code")
|> normalize_integer_field("error_code")

Map.put(trigger, "sync_webhook_response_config", normalized)
end

defp normalize_sync_webhook_response_config(trigger), do: trigger

defp normalize_integer_field(map, key) do
case Map.fetch(map, key) do
{:ok, value} when is_float(value) -> Map.put(map, key, trunc(value))
_ -> map
end
end

defp extract_positions(positions_map) do
Yex.Map.to_json(positions_map)
end
Expand Down
34 changes: 29 additions & 5 deletions lib/lightning/export_utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ defmodule Lightning.ExportUtils do
trigger: [
:type,
:webhook_reply,
:webhook_response,
:cron_expression,
:cron_cursor_job,
:enabled,
Expand Down Expand Up @@ -121,14 +122,37 @@ defmodule Lightning.ExportUtils do
Map.put(base, :kafka_configuration, kafka_config)

:webhook ->
if trigger.webhook_reply do
Map.put(base, :webhook_reply, Atom.to_string(trigger.webhook_reply))
else
base
end
base
|> maybe_put_webhook_reply(trigger.webhook_reply)
|> maybe_put_webhook_response(trigger.sync_webhook_response_config)
end
end

defp maybe_put_webhook_reply(map, nil), do: map

defp maybe_put_webhook_reply(map, reply) when is_atom(reply) do
Map.put(map, :webhook_reply, Atom.to_string(reply))
end

defp maybe_put_webhook_response(map, %{} = config) do
webhook_response =
Map.reject(
%{
success_code: config.success_code,
error_code: config.error_code
},
fn {_k, v} -> is_nil(v) end
)

if map_size(webhook_response) > 0 do
Map.put(map, :webhook_response, webhook_response)
else
map
end
end

defp maybe_put_webhook_response(map, _), do: map

defp edge_to_treenode(%{source_job_id: nil} = edge, triggers, jobs) do
source_trigger =
Enum.find(triggers, fn t -> t.id == edge.source_trigger_id end)
Expand Down
18 changes: 18 additions & 0 deletions lib/lightning/projects/provisioner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ defmodule Lightning.Projects.Provisioner do
alias Lightning.Workflows.Snapshot
alias Lightning.Workflows.Trigger
alias Lightning.Workflows.Triggers.KafkaConfiguration
alias Lightning.Workflows.Triggers.SyncWebhookResponseConfig
alias Lightning.Workflows.Workflow
alias Lightning.Workflows.WorkflowUsageLimiter
alias Lightning.WorkflowVersions
Expand Down Expand Up @@ -458,13 +459,20 @@ defmodule Lightning.Projects.Provisioner do
end

defp trigger_changeset(trigger, attrs) do
attrs = remap_webhook_response(attrs)

trigger
|> Trigger.cast_changeset(attrs)
|> cast_embed(
:kafka_configuration,
required: false,
with: &kafka_config_changeset/2
)
|> cast_embed(
:sync_webhook_response_config,
required: false,
with: &SyncWebhookResponseConfig.changeset/2
)
|> Trigger.validate()
|> cast(attrs, [:delete])
|> validate_required([:id])
Expand All @@ -473,6 +481,16 @@ defmodule Lightning.Projects.Provisioner do
|> maybe_mark_for_deletion()
end

defp remap_webhook_response(attrs) do
case Map.pop(attrs, "webhook_response") do
{nil, attrs} ->
attrs

{config, attrs} ->
Map.put(attrs, "sync_webhook_response_config", config)
end
end

defp kafka_config_changeset(kafka_config, attrs) do
kafka_config
|> KafkaConfiguration.changeset(attrs)
Expand Down
Loading