Create trigger payload validation, refacto HttpEndpoint validator and create Cron validator (#6641)

This commit is contained in:
jeanbaptistedalle 2025-05-20 19:52:43 +02:00 committed by GitHub
parent 9cbf75115f
commit c9bdcd5a84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 839 additions and 63 deletions

View file

@ -3,6 +3,7 @@ using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Http.Bookmarks;
using Elsa.Http.ContentWriters;
using Elsa.Http.DownloadableContentHandlers;
using Elsa.Http.FileCaches;
@ -14,6 +15,7 @@ using Elsa.Http.Resilience;
using Elsa.Http.Selectors;
using Elsa.Http.Services;
using Elsa.Http.Tasks;
using Elsa.Http.TriggerPayloadValidators;
using Elsa.Http.UIHints;
using Elsa.Resilience.Extensions;
using Elsa.Resilience.Features;
@ -172,7 +174,6 @@ public class HttpFeature(IModule module) : FeatureBase(module)
.AddHttpContextAccessor()
// Handlers.
.AddNotificationHandler<ValidateWorkflowRequestHandler>()
.AddNotificationHandler<UpdateRouteTable>()
// Content parsers.
@ -220,6 +221,9 @@ public class HttpFeature(IModule module) : FeatureBase(module)
.AddScoped<IDownloadableContentHandler, StringDownloadableContentHandler>()
.AddScoped<IDownloadableContentHandler, HttpFileDownloadableContentHandler>()
//Trigger payload validators.
.AddTriggerPaylodValidator<HttpEndpointTriggerPayloadValidator, HttpEndpointBookmarkPayload>()
// File caches.
.AddScoped(FileCache)
.AddScoped<ZipManager>()

View file

@ -1,60 +0,0 @@
using Elsa.Extensions;
using Elsa.Http.Bookmarks;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Management.Notifications;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Filters;
using JetBrains.Annotations;
namespace Elsa.Http.Handlers;
/// <summary>
/// A <see cref="ValidateWorkflowRequest"/> handler that validates a workflow path and return any errors.
/// </summary>
[UsedImplicitly]
public class ValidateWorkflowRequestHandler : INotificationHandler<WorkflowDefinitionValidating>
{
private readonly ITriggerStore _triggerStore;
private readonly ITriggerIndexer _triggerIndexer;
/// <summary>
/// Constructor.
/// </summary>
public ValidateWorkflowRequestHandler(ITriggerStore triggerStore, ITriggerIndexer triggerIndexer)
{
_triggerStore = triggerStore;
_triggerIndexer = triggerIndexer;
}
public async Task HandleAsync(WorkflowDefinitionValidating notification, CancellationToken cancellationToken)
{
var workflow = notification.Workflow;
var httpEndpointTriggers = (await _triggerIndexer.GetTriggersAsync(workflow, cancellationToken)).Where(x => x.Payload is HttpEndpointBookmarkPayload).ToList();
var filter = new TriggerFilter
{
Name = ActivityTypeNameHelper.GenerateTypeName(typeof(HttpEndpoint))
};
var publishedWorkflowsTriggers = (await _triggerStore.FindManyAsync(filter, cancellationToken)).ToList();
var validationErrors = notification.ValidationErrors;
foreach (var httpEndpointTrigger in httpEndpointTriggers)
{
var triggerPayload = httpEndpointTrigger.GetPayload<HttpEndpointBookmarkPayload>();
var otherWorkflowsWithSamePath = publishedWorkflowsTriggers
.Where(x =>
x.WorkflowDefinitionId != workflow.Identity.DefinitionId &&
x.Payload is HttpEndpointBookmarkPayload payload &&
payload.Path == triggerPayload.Path &&
payload.Method == triggerPayload.Method)
.ToList();
if (!otherWorkflowsWithSamePath.Any())
continue;
var message = $"The {triggerPayload.Path} path and {triggerPayload.Method} method are already in use by another workflow!";
validationErrors.Add(new(message, httpEndpointTrigger.ActivityId));
}
}
}

View file

@ -0,0 +1,41 @@
using Elsa.Http.Bookmarks;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Filters;
namespace Elsa.Http.TriggerPayloadValidators;
public class HttpEndpointTriggerPayloadValidator(ITriggerStore triggerStore) : ITriggerPayloadValidator<HttpEndpointBookmarkPayload>
{
public async Task ValidateAsync(
HttpEndpointBookmarkPayload payload,
Workflow workflow,
StoredTrigger trigger,
ICollection<WorkflowValidationError> validationErrors,
CancellationToken cancellationToken)
{
var filter = new TriggerFilter
{
Name = ActivityTypeNameHelper.GenerateTypeName(typeof(HttpEndpoint))
};
var publishedWorkflowsTriggers = (await triggerStore.FindManyAsync(filter, cancellationToken)).ToList();
var otherWorkflowsWithSamePath = publishedWorkflowsTriggers
.Where(x =>
x.WorkflowDefinitionId != workflow.Identity.DefinitionId &&
x.Payload is HttpEndpointBookmarkPayload anotherHttpEndpointPayload &&
anotherHttpEndpointPayload.Path == payload.Path &&
anotherHttpEndpointPayload.Method == payload.Method)
.ToList();
if (otherWorkflowsWithSamePath.Count == 0)
return;
validationErrors.Add(new($"The {payload.Path} path and {payload.Method} method are already in use by another workflow!",
trigger.ActivityId));
}
}

View file

@ -1,3 +1,3 @@
namespace Elsa.Scheduling.Bookmarks;
internal record CronTriggerPayload(string CronExpression);
public record CronTriggerPayload(string CronExpression);

View file

@ -4,9 +4,11 @@ using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Scheduling.Bookmarks;
using Elsa.Scheduling.Handlers;
using Elsa.Scheduling.HostedServices;
using Elsa.Scheduling.Services;
using Elsa.Scheduling.TriggerPayloadValidators;
using Elsa.Workflows.Management.Features;
using Microsoft.Extensions.DependencyInjection;
@ -49,7 +51,10 @@ public class SchedulingFeature : FeatureBase
.AddSingleton(CronParser)
.AddScoped(WorkflowScheduler)
.AddBackgroundTask<CreateSchedulesBackgroundTask>()
.AddHandlersFrom<ScheduleWorkflows>();
.AddHandlersFrom<ScheduleWorkflows>()
//Trigger payload validators.
.AddTriggerPaylodValidator<CronTriggerPayloadValidator, CronTriggerPayload>();
Module.Configure<WorkflowManagementFeature>(management => management.AddActivitiesFrom<SchedulingFeature>());
}

View file

@ -0,0 +1,30 @@
using Cronos;
using Elsa.Scheduling.Bookmarks;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Entities;
namespace Elsa.Scheduling.TriggerPayloadValidators;
public class CronTriggerPayloadValidator(ICronParser cronParser) : ITriggerPayloadValidator<CronTriggerPayload>
{
public Task ValidateAsync(
CronTriggerPayload payload,
Workflow workflow,
StoredTrigger trigger,
ICollection<WorkflowValidationError> validationErrors,
CancellationToken cancellationToken)
{
try
{
cronParser.GetNextOccurrence(payload.CronExpression);
}
catch (CronFormatException ex)
{
validationErrors.Add(new("Error when parsing cron expression: " + ex.Message,
trigger.ActivityId));
}
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,21 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Runtime.Entities;
namespace Elsa.Workflows.Runtime.Contracts;
/// <summary>
/// Validator that validate a given trigger payload.
/// </summary>
public interface ITriggerPayloadValidator<TPayload>
{
/// <summary>
/// Validate a trigger payload. If trigger is not valid, error will be add in this list.
/// </summary>
Task ValidateAsync(
TPayload payload,
Workflow workflow,
StoredTrigger trigger,
ICollection<WorkflowValidationError> validationErrors,
CancellationToken cancellationToken);
}

View file

@ -1,4 +1,5 @@
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Providers;
// ReSharper disable once CheckNamespace
@ -16,4 +17,16 @@ public static class DependencyInjectionExtensions
/// <typeparam name="T">The type of the workflow definition provider.</typeparam>
/// <returns>The service collection.</returns>
public static IServiceCollection AddWorkflowDefinitionProvider<T>(this IServiceCollection services) where T : class, IWorkflowsProvider => services.AddScoped<IWorkflowsProvider, T>();
/// <summary>
/// Registers a <see cref="ITriggerPayloadValidator{TPayload}"/> with the service container.
/// </summary>
/// <param name="services">Service collection</param>
/// <typeparam name="TValidator">Validator of the validator</typeparam>
/// <typeparam name="TPayload">Payload type</typeparam>
public static IServiceCollection AddTriggerPaylodValidator<TValidator, TPayload>(this IServiceCollection services)
where TValidator : class, ITriggerPayloadValidator<TPayload>
{
return services.AddScoped<ITriggerPayloadValidator<TPayload>, TValidator>();
}
}

View file

@ -345,6 +345,7 @@ public class WorkflowRuntimeFeature(IModule module) : FeatureBase(module)
.AddNotificationHandler<RefreshActivityRegistry>()
.AddNotificationHandler<SignalBookmarkQueueWorker>()
.AddNotificationHandler<EvaluateParentLogPersistenceModes>()
.AddNotificationHandler<ValidateWorkflowRequestHandler>()
// Workflow activation strategies.
.AddScoped<IWorkflowActivationStrategy, SingletonStrategy>()

View file

@ -0,0 +1,58 @@
using System.Reflection;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Management.Notifications;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Entities;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.Runtime.Handlers;
/// <summary>
/// A <see cref="WorkflowDefinitionValidating"/> handler that validates a workflow path and return any errors.
/// </summary>
[UsedImplicitly]
public class ValidateWorkflowRequestHandler(
ITriggerIndexer triggerIndexer,
IServiceProvider serviceProvider) : INotificationHandler<WorkflowDefinitionValidating>
{
public async Task HandleAsync(WorkflowDefinitionValidating notification, CancellationToken cancellationToken)
{
Workflow workflow = notification.Workflow;
ICollection<WorkflowValidationError> validationErrors = notification.ValidationErrors;
foreach (StoredTrigger trigger in await triggerIndexer.GetTriggersAsync(workflow, cancellationToken))
{
if (trigger.Payload is null)
{
validationErrors.Add(new($"Trigger should have a payload",
trigger.ActivityId));
}
else
{
MethodInfo method = typeof(ValidateWorkflowRequestHandler)
.GetMethod(nameof(ValidateInternalAsync), BindingFlags.Instance | BindingFlags.NonPublic)!
.MakeGenericMethod(trigger.Payload.GetType());
object? result = method.Invoke(this, [trigger.Payload, workflow, trigger, validationErrors, cancellationToken]);
if (result is Task task)
{
await task;
}
}
}
}
private async Task ValidateInternalAsync<TPayload>(TPayload instance,
Workflow workflow,
StoredTrigger trigger,
ICollection<WorkflowValidationError> validationErrors,
CancellationToken cancellationToken)
{
ITriggerPayloadValidator<TPayload>? payloadValidator = serviceProvider.GetService<ITriggerPayloadValidator<TPayload>>();
if (payloadValidator != null)
await payloadValidator.ValidateAsync(instance, workflow, trigger, validationErrors, cancellationToken);
}
}

View file

@ -0,0 +1,56 @@
using Elsa.Testing.Shared;
using Elsa.Workflows.Management;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Scenarios.ImportAndPublish;
public class ImportAndPublishCronTests
{
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IServiceProvider _services;
public ImportAndPublishCronTests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper)
.WithCapturingTextWriter(_capturingTextWriter)
.Build();
}
[Fact(DisplayName = "Cron workflow imported from file should publish successfully.")]
public async Task ImportAndPublish_ShouldSucceed_WithGoodCron()
{
// Populate registries.
await _services.PopulateRegistriesAsync();
// Import workflow.
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImportAndPublish/Workflows/cron-every-hour.json");
// Publish.
IWorkflowDefinitionPublisher workflowDefinitionPublisher = _services.GetRequiredService<IWorkflowDefinitionPublisher>();
var result = await workflowDefinitionPublisher.PublishAsync(workflowDefinition);
// Assert.
Assert.True(result.Succeeded);
Assert.Empty(result.ValidationErrors);
}
[Fact(DisplayName = "Cron workflow imported from file should not publish successfully with bad cron expression.")]
public async Task ImportAndPublish_ShouldFailed_WithBadCronExpression()
{
// Populate registries.
await _services.PopulateRegistriesAsync();
// Import workflow.
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImportAndPublish/Workflows/bad-cron-expression.json");
// Publish.
IWorkflowDefinitionPublisher workflowDefinitionPublisher = _services.GetRequiredService<IWorkflowDefinitionPublisher>();
var result = await workflowDefinitionPublisher.PublishAsync(workflowDefinition);
// Assert.
Assert.False(result.Succeeded);
Assert.Single(result.ValidationErrors);
Assert.Equal("Error when parsing cron expression: The given cron expression has an invalid format. Seconds: Value must be a number between 0 and 59 (all inclusive).", result.ValidationErrors.Single().Message);
}
}

View file

@ -0,0 +1,68 @@
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows.Management;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Scenarios.ImportAndPublish;
public class ImportAndPublishHttpEndpointsTests
{
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IServiceProvider _services;
public ImportAndPublishHttpEndpointsTests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper)
.WithCapturingTextWriter(_capturingTextWriter)
.ConfigureElsa(configure => configure.UseHttp())
.Build();
}
[Fact(DisplayName = "Http endpoint workflow imported from file should publish successfully.")]
public async Task ImportAndPublish_ShouldSucceed_WithGoodHttpEndpoint()
{
// Populate registries.
await _services.PopulateRegistriesAsync();
// Import workflow.
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImportAndPublish/Workflows/http-workflow.json");
// Publish.
IWorkflowDefinitionPublisher workflowDefinitionPublisher = _services.GetRequiredService<IWorkflowDefinitionPublisher>();
var result = await workflowDefinitionPublisher.PublishAsync(workflowDefinition);
// Assert.
Assert.True(result.Succeeded);
Assert.Empty(result.ValidationErrors);
}
[Fact(DisplayName = "Http endpoint workflow imported from file should not publish successfully with same path and method.")]
public async Task ImportAndPublish_ShouldFailed_WithTwoHttpEndpointSamePathMethod()
{
// Populate registries.
await _services.PopulateRegistriesAsync();
// Import first workflow.
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImportAndPublish/Workflows/http-workflow.json");
// Publish first workflow.
IWorkflowDefinitionPublisher workflowDefinitionPublisher = _services.GetRequiredService<IWorkflowDefinitionPublisher>();
var result = await workflowDefinitionPublisher.PublishAsync(workflowDefinition);
// Assert first workflow.
Assert.True(result.Succeeded);
Assert.Empty(result.ValidationErrors);
// Import second workflow.
workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImportAndPublish/Workflows/http-workflow.json");
// Publish second workflow.
result = await workflowDefinitionPublisher.PublishAsync(workflowDefinition);
// Assert second workflow.
Assert.False(result.Succeeded);
Assert.Single(result.ValidationErrors);
Assert.Equal("The /test path and get method are already in use by another workflow!", result.ValidationErrors.Single().Message);
}
}

View file

@ -0,0 +1,37 @@
using Elsa.Testing.Shared;
using Elsa.Workflows.Management;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Scenarios.ImportAndPublish;
public class ImportAndPublishTimerTests
{
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IServiceProvider _services;
public ImportAndPublishTimerTests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper)
.WithCapturingTextWriter(_capturingTextWriter)
.Build();
}
[Fact(DisplayName = "Timer workflow imported from file should publish successfully.")]
public async Task ImportAndPublish_ShouldSucceed_WithGoodTimerWithoutValidator()
{
// Populate registries.
await _services.PopulateRegistriesAsync();
// Import workflow.
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImportAndPublish/Workflows/timer-workflow.json");
// Publish.
IWorkflowDefinitionPublisher workflowDefinitionPublisher = _services.GetRequiredService<IWorkflowDefinitionPublisher>();
var result = await workflowDefinitionPublisher.PublishAsync(workflowDefinition);
// Assert.
Assert.True(result.Succeeded);
Assert.Empty(result.ValidationErrors);
}
}

View file

@ -0,0 +1,117 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "868f0fa68d846638",
"definitionId": "311cedd544cfe691",
"name": "CronWorkflow",
"createdAt": "2025-05-12T12:16:06.603274+00:00",
"version": 1,
"toolVersion": "3.3.0.0",
"variables": [
],
"inputs": [
],
"outputs": [
],
"outcomes": [
],
"customProperties": {
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": false,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"id": "57363d0e76c3e9d2",
"nodeId": "Workflow1:57363d0e76c3e9d2",
"name": "Flowchart1",
"type": "Elsa.Flowchart",
"version": 1,
"customProperties": {
"notFoundConnections": [
],
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
},
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Hello every hour"
}
},
"id": "1bf2412c9603f5d3",
"nodeId": "Workflow1:57363d0e76c3e9d2:1bf2412c9603f5d3",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -39.899993896484375,
"y": -88.600006103515625
},
"size": {
"width": 138.02500915527344,
"height": 49.600002288818359
}
}
}
},
{
"cronExpression": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "BadCronExpression"
}
},
"id": "13658e4b3bfafaaf",
"nodeId": "Workflow1:57363d0e76c3e9d2:13658e4b3bfafaaf",
"name": "Cron1",
"type": "Elsa.Cron",
"version": 1,
"customProperties": {
"canStartWorkflow": true,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -206.89999389648438,
"y": -88.600006103515625
},
"size": {
"width": 100.59999847412109,
"height": 49.600002288818359
}
}
}
}
],
"variables": [
],
"connections": [
{
"source": {
"activity": "13658e4b3bfafaaf",
"port": "Done"
},
"target": {
"activity": "1bf2412c9603f5d3",
"port": "In"
}
}
]
}
}

View file

@ -0,0 +1,117 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "868f0fa68d846638",
"definitionId": "311cedd544cfe691",
"name": "CronWorkflow",
"createdAt": "2025-05-12T12:16:06.603274+00:00",
"version": 1,
"toolVersion": "3.3.0.0",
"variables": [
],
"inputs": [
],
"outputs": [
],
"outcomes": [
],
"customProperties": {
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": false,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"id": "57363d0e76c3e9d2",
"nodeId": "Workflow1:57363d0e76c3e9d2",
"name": "Flowchart1",
"type": "Elsa.Flowchart",
"version": 1,
"customProperties": {
"notFoundConnections": [
],
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
},
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Hello every hour"
}
},
"id": "1bf2412c9603f5d3",
"nodeId": "Workflow1:57363d0e76c3e9d2:1bf2412c9603f5d3",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -39.899993896484375,
"y": -88.600006103515625
},
"size": {
"width": 138.02500915527344,
"height": 49.600002288818359
}
}
}
},
{
"cronExpression": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "0 0 * * * *"
}
},
"id": "13658e4b3bfafaaf",
"nodeId": "Workflow1:57363d0e76c3e9d2:13658e4b3bfafaaf",
"name": "Cron1",
"type": "Elsa.Cron",
"version": 1,
"customProperties": {
"canStartWorkflow": true,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -206.89999389648438,
"y": -88.600006103515625
},
"size": {
"width": 100.59999847412109,
"height": 49.600002288818359
}
}
}
}
],
"variables": [
],
"connections": [
{
"source": {
"activity": "13658e4b3bfafaaf",
"port": "Done"
},
"target": {
"activity": "1bf2412c9603f5d3",
"port": "In"
}
}
]
}
}

View file

@ -0,0 +1,151 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"name": "HttpWorkflow",
"createdAt": "2025-05-12T12:28:56.845918+00:00",
"version": 1,
"toolVersion": "3.3.0.0",
"variables": [
],
"inputs": [
],
"outputs": [
],
"outcomes": [
],
"customProperties": {
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": false,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"id": "8163a86bbcff6777",
"nodeId": "Workflow1:8163a86bbcff6777",
"name": "Flowchart1",
"type": "Elsa.Flowchart",
"version": 1,
"customProperties": {
"notFoundConnections": [
],
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
},
"activities": [
{
"path": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "test"
}
},
"supportedMethods": {
"typeName": "List<String>",
"expression": {
"type": "Literal",
"value": "[\"GET\"]"
}
},
"authorize": {
"typeName": "Boolean",
"expression": {
"type": "Literal",
"value": false
}
},
"policy": {
"typeName": "String",
"expression": {
"type": "Literal"
}
},
"requestTimeout": null,
"requestSizeLimit": null,
"fileSizeLimit": null,
"allowedFileExtensions": null,
"blockedFileExtensions": null,
"allowedMimeTypes": null,
"exposeRequestTooLargeOutcome": false,
"exposeFileTooLargeOutcome": false,
"exposeInvalidFileExtensionOutcome": false,
"exposeInvalidFileMimeTypeOutcome": false,
"parsedContent": null,
"files": null,
"routeData": null,
"queryStringData": null,
"headers": null,
"result": null,
"id": "b036c71bbd31983d",
"nodeId": "Workflow1:8163a86bbcff6777:b036c71bbd31983d",
"name": "HttpEndpoint1",
"type": "Elsa.HttpEndpoint",
"version": 1,
"customProperties": {
"canStartWorkflow": true,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -180,
"y": -80
},
"size": {
"width": 173.5625,
"height": 49.600002288818359
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Hello from http"
}
},
"id": "b91befb34b3e44ea",
"nodeId": "Workflow1:8163a86bbcff6777:b91befb34b3e44ea",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 133.0999755859375,
"y": -80
},
"size": {
"width": 138.02500915527344,
"height": 49.600002288818359
}
}
}
}
],
"variables": [
],
"connections": [
{
"source": {
"activity": "b036c71bbd31983d",
"port": "Done"
},
"target": {
"activity": "b91befb34b3e44ea",
"port": "In"
}
}
]
}
}

View file

@ -0,0 +1,117 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "6e217c3e1fbce6e",
"definitionId": "d1d828033d9c2dcd",
"name": "TimerWorkflow",
"createdAt": "2025-05-12T12:28:56.845918+00:00",
"version": 1,
"toolVersion": "3.3.0.0",
"variables": [
],
"inputs": [
],
"outputs": [
],
"outcomes": [
],
"customProperties": {
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": false,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"id": "8163a86bbcff6777",
"nodeId": "Workflow1:8163a86bbcff6777",
"name": "Flowchart1",
"type": "Elsa.Flowchart",
"version": 1,
"customProperties": {
"notFoundConnections": [
],
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
},
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Hello from timer"
}
},
"id": "b91befb34b3e44ea",
"nodeId": "Workflow1:8163a86bbcff6777:b91befb34b3e44ea",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 133.0999755859375,
"y": -80
},
"size": {
"width": 138.02500915527344,
"height": 49.600002288818359
}
}
}
},
{
"interval": {
"typeName": "TimeSpan",
"expression": {
"type": "Literal",
"value": "00:01:00"
}
},
"id": "89ab56281a99239",
"nodeId": "Workflow1:8163a86bbcff6777:89ab56281a99239",
"name": "Timer1",
"type": "Elsa.Timer",
"version": 1,
"customProperties": {
"canStartWorkflow": true,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -81.899993896484375,
"y": -80
},
"size": {
"width": 107.63750457763672,
"height": 49.600002288818359
}
}
}
}
],
"variables": [
],
"connections": [
{
"source": {
"activity": "89ab56281a99239",
"port": "Done"
},
"target": {
"activity": "b91befb34b3e44ea",
"port": "In"
}
}
]
}
}