ironflow_engine/operation.rs
1//! [`Operation`] trait — user-defined step operations.
2//!
3//! Implement this trait to create custom step types that integrate into the
4//! workflow lifecycle. Common use cases include API clients (GitLab, Gmail,
5//! Slack) that need full step tracking (persistence, duration, error handling).
6//!
7//! # How it works
8//!
9//! 1. Implement [`Operation`] on your type.
10//! 2. Call [`WorkflowContext::operation()`](crate::context::WorkflowContext::operation)
11//! inside a [`WorkflowHandler`](crate::handler::WorkflowHandler).
12//! 3. The engine handles the full step lifecycle: create step record, transition
13//! to Running, execute, persist output/duration, mark Completed or Failed.
14//!
15//! # Examples
16//!
17//! ```no_run
18//! use ironflow_engine::operation::Operation;
19//! use ironflow_engine::error::EngineError;
20//! use serde_json::{Value, json};
21//! use std::future::Future;
22//! use std::pin::Pin;
23//!
24//! struct CreateGitlabIssue {
25//! project_id: u64,
26//! title: String,
27//! }
28//!
29//! impl Operation for CreateGitlabIssue {
30//! fn kind(&self) -> &str {
31//! "gitlab"
32//! }
33//!
34//! fn execute(&self) -> Pin<Box<dyn Future<Output = Result<Value, EngineError>> + Send + '_>> {
35//! Box::pin(async move {
36//! // Call the GitLab API here (e.g. via the `gitlab` crate).
37//! Ok(json!({"issue_id": 42, "url": "https://gitlab.com/issues/42"}))
38//! })
39//! }
40//! }
41//! ```
42
43use std::future::Future;
44use std::pin::Pin;
45
46use serde_json::Value;
47
48use crate::error::EngineError;
49
50/// A user-defined operation that integrates into the workflow step lifecycle.
51///
52/// Implement this trait for custom integrations (GitLab, Gmail, Slack, etc.)
53/// that need full step tracking when executed via
54/// [`WorkflowContext::operation()`](crate::context::WorkflowContext::operation).
55///
56/// # Contract
57///
58/// - [`kind()`](Operation::kind) returns a short, lowercase identifier stored
59/// as [`StepKind::Custom`](ironflow_store::entities::StepKind::Custom) in
60/// the database (e.g. `"gitlab"`, `"gmail"`, `"slack"`).
61/// - [`execute()`](Operation::execute) performs the operation and returns
62/// a JSON [`Value`] on success. The engine persists this as the step output.
63///
64/// # Examples
65///
66/// ```no_run
67/// use ironflow_engine::operation::Operation;
68/// use ironflow_engine::error::EngineError;
69/// use serde_json::{Value, json};
70/// use std::future::Future;
71/// use std::pin::Pin;
72///
73/// struct SendSlackMessage {
74/// channel: String,
75/// text: String,
76/// }
77///
78/// impl Operation for SendSlackMessage {
79/// fn kind(&self) -> &str { "slack" }
80///
81/// fn execute(&self) -> Pin<Box<dyn Future<Output = Result<Value, EngineError>> + Send + '_>> {
82/// Box::pin(async move {
83/// // Post to Slack API ...
84/// Ok(json!({"ok": true, "ts": "1234567890.123456"}))
85/// })
86/// }
87/// }
88/// ```
89pub trait Operation: Send + Sync {
90 /// A short, lowercase identifier for this operation type.
91 ///
92 /// Stored as [`StepKind::Custom(kind)`](ironflow_store::entities::StepKind::Custom)
93 /// in the database. Examples: `"gitlab"`, `"gmail"`, `"slack"`.
94 fn kind(&self) -> &str;
95
96 /// Execute the operation and return the result as JSON.
97 ///
98 /// The returned [`Value`] is persisted as the step output. On error,
99 /// the engine marks the step as Failed and records the error message.
100 ///
101 /// # Errors
102 ///
103 /// Return [`EngineError`] if the operation fails.
104 fn execute(&self) -> Pin<Box<dyn Future<Output = Result<Value, EngineError>> + Send + '_>>;
105
106 /// Optional JSON representation of the operation input, stored in
107 /// the step's `input` column for observability.
108 ///
109 /// Defaults to [`None`]. Override to provide structured input logging.
110 ///
111 /// # Examples
112 ///
113 /// ```no_run
114 /// # use ironflow_engine::operation::Operation;
115 /// # use ironflow_engine::error::EngineError;
116 /// # use serde_json::{Value, json};
117 /// # use std::pin::Pin;
118 /// # use std::future::Future;
119 /// # struct MyOp;
120 /// # impl Operation for MyOp {
121 /// # fn kind(&self) -> &str { "test" }
122 /// # fn execute(&self) -> Pin<Box<dyn Future<Output = Result<Value, EngineError>> + Send + '_>> {
123 /// # Box::pin(async { Ok(json!({})) })
124 /// # }
125 /// fn input(&self) -> Option<Value> {
126 /// Some(json!({"project_id": 123, "title": "Bug report"}))
127 /// }
128 /// # }
129 /// ```
130 fn input(&self) -> Option<Value> {
131 None
132 }
133}