Skip to main content

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}