diff --git a/Cargo.toml b/Cargo.toml
index f447260417509377666fb4a95b00ef2cde071b93..f873155c44d93df91b9dcc9811c6b7e151a5088b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -52,3 +52,4 @@ configuration = { package = "zombienet-configuration", version = "0.1.0-alpha.0"
 orchestrator = { package = "zombienet-orchestrator", version = "0.1.0-alpha.0", path = "crates/orchestrator" }
 provider = { package = "zombienet-provider", version = "0.1.0-alpha.0", path = "crates/provider" }
 prom-metrics-parser = { package = "zombienet-prom-metrics-parser", version = "0.1.0-alpha.0", path = "crates/prom-metrics-parser" }
+sdk = { package = "zombienet-sdk", version = "0.1.0-alpha.0", path = "crates/sdk" }
diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml
index ca65ee13c0e7f78b08b22caf9391454464cb23b9..0dd7f343b211fc1cae31666735c50c75406f156c 100644
--- a/crates/examples/Cargo.toml
+++ b/crates/examples/Cargo.toml
@@ -10,3 +10,4 @@ zombienet-sdk = { path = "../sdk" }
 tokio = { workspace = true }
 futures = { workspace = true }
 subxt = { workspace = true }
+tracing-subscriber = "0.3"
diff --git a/crates/examples/examples/simple_network_example.rs b/crates/examples/examples/simple_network_example.rs
index 5464c990d3ee86bfcd20751da31a50094adf5ae8..0e6cf9306b70340966eb082d1946e06d8c99dde8 100644
--- a/crates/examples/examples/simple_network_example.rs
+++ b/crates/examples/examples/simple_network_example.rs
@@ -3,6 +3,7 @@ use zombienet_sdk::{NetworkConfig, NetworkConfigExt};
 
 #[tokio::main]
 async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    tracing_subscriber::fmt::init();
     let network = NetworkConfig::load_from_toml("./crates/examples/examples/0001-simple.toml")
         .expect("errored?")
         .spawn_native()
diff --git a/crates/examples/examples/small_network_with_default.rs b/crates/examples/examples/small_network_with_default.rs
index 5a32d764f1074b3ffcde43c9dc330fd13c6eb239..93fdeac141333b7249f693f7bdbfd03114a77c88 100644
--- a/crates/examples/examples/small_network_with_default.rs
+++ b/crates/examples/examples/small_network_with_default.rs
@@ -1,7 +1,8 @@
-use zombienet_sdk::{AddNodeOpts, NetworkConfigBuilder, NetworkConfigExt};
+use zombienet_sdk::{AddCollatorOptions, AddNodeOptions, NetworkConfigBuilder, NetworkConfigExt};
 
 #[tokio::main]
 async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    tracing_subscriber::fmt::init();
     let mut network = NetworkConfigBuilder::new()
         .with_relaychain(|r| {
             r.with_chain("rococo-local")
@@ -20,15 +21,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
         .await?;
 
     println!("🚀🚀🚀🚀 network deployed");
-    // add  a new node
-    let opts = AddNodeOpts {
+
+    // Add a new node to the running network.
+    let opts = AddNodeOptions {
         rpc_port: Some(9444),
         is_validator: true,
         ..Default::default()
     };
 
-    // TODO: add check to ensure if unique
-    network.add_node("new1", opts, None).await?;
+    network.add_node("new1", opts).await?;
 
     // Example of some operations that you can do
     // with `nodes` (e.g pause, resume, restart)
@@ -49,11 +50,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     // node.resume().await?;
     // println!("node new1 resumed!");
 
-    let col_opts = AddNodeOpts {
+    let col_opts = AddCollatorOptions {
         command: Some("polkadot-parachain".try_into()?),
         ..Default::default()
     };
-    network.add_node("new-col-1", col_opts, Some(100)).await?;
+    network.add_collator("new-col-1", col_opts, 100).await?;
     println!("new collator deployed!");
 
     // For now let just loop....
diff --git a/crates/examples/examples/small_network_with_para.rs b/crates/examples/examples/small_network_with_para.rs
index d11b8a36cb2eeff85f364146c4e851ea25a54af4..5c332728d71bed9eb38c47f66fb7126d77edfb86 100644
--- a/crates/examples/examples/small_network_with_para.rs
+++ b/crates/examples/examples/small_network_with_para.rs
@@ -1,9 +1,13 @@
 use std::time::Duration;
 
-use zombienet_sdk::{AddNodeOpts, NetworkConfigBuilder, NetworkConfigExt, RegistrationStrategy};
+use zombienet_sdk::{
+    AddCollatorOptions, AddNodeOptions, NetworkConfigBuilder, NetworkConfigExt,
+    RegistrationStrategy,
+};
 
 #[tokio::main]
 async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    tracing_subscriber::fmt::init();
     let mut network = NetworkConfigBuilder::new()
         .with_relaychain(|r| {
             r.with_chain("rococo-local")
@@ -24,14 +28,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
 
     println!("🚀🚀🚀🚀 network deployed");
     // add  a new node
-    let opts = AddNodeOpts {
+    let opts = AddNodeOptions {
         rpc_port: Some(9444),
         is_validator: true,
         ..Default::default()
     };
 
     // TODO: add check to ensure if unique
-    network.add_node("new1", opts, None).await?;
+    network.add_node("new1", opts).await?;
 
     tokio::time::sleep(Duration::from_secs(2)).await;
 
@@ -58,11 +62,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     // node.resume().await?;
     // println!("node new1 resumed!");
 
-    let col_opts = AddNodeOpts {
+    let col_opts = AddCollatorOptions {
         command: Some("polkadot-parachain".try_into()?),
         ..Default::default()
     };
-    network.add_node("new-col-1", col_opts, Some(100)).await?;
+    network.add_collator("new-col-1", col_opts, 100).await?;
     println!("new collator deployed!");
 
     // For now let just loop....
diff --git a/crates/orchestrator/src/generators/chain_spec.rs b/crates/orchestrator/src/generators/chain_spec.rs
index 27e96c6bf82dfca2f2a336c839ef4e3fe8d22903..d44dab28a29db0f7c1511c2f3e211954becacdf3 100644
--- a/crates/orchestrator/src/generators/chain_spec.rs
+++ b/crates/orchestrator/src/generators/chain_spec.rs
@@ -11,7 +11,7 @@ use provider::{
 };
 use serde_json::json;
 use support::fs::FileSystem;
-use tracing::warn;
+use tracing::{debug, warn};
 
 use super::errors::GeneratorError;
 use crate::{
@@ -811,7 +811,7 @@ fn add_collator_selection(
             *invulnerables = json!(keys);
         } else {
             // TODO: add a nice warning here.
-            println!("warn!! can't customize the invulnerables key");
+            debug!("⚠️  'invulnerables' not present in spec, will not be customized");
         }
     } else {
         unreachable!("pointer to runtime config should be valid!")
diff --git a/crates/orchestrator/src/lib.rs b/crates/orchestrator/src/lib.rs
index 992112c3ad7523e220bfb1cd9ec6037ac47d4bce..b2d42c3b3dacd0580607fadb2ffa68a552ab4ee9 100644
--- a/crates/orchestrator/src/lib.rs
+++ b/crates/orchestrator/src/lib.rs
@@ -458,4 +458,4 @@ pub enum ZombieRole {
 }
 
 // re-export
-pub use network::AddNodeOpts;
+pub use network::{AddCollatorOptions, AddNodeOptions};
diff --git a/crates/orchestrator/src/network.rs b/crates/orchestrator/src/network.rs
index 20edbd8315f88d6740838e8124b9f0b66cd88c07..0b981e44c20345b377d7f10e6e4e4b196540eaef 100644
--- a/crates/orchestrator/src/network.rs
+++ b/crates/orchestrator/src/network.rs
@@ -14,7 +14,7 @@ use support::fs::FileSystem;
 use self::{node::NetworkNode, parachain::Parachain, relaychain::Relaychain};
 use crate::{
     network_spec::{self, NetworkSpec},
-    shared::types::ChainDefaultContext,
+    shared::{macros, types::ChainDefaultContext},
     spawner::{self, SpawnNodeCtx},
     ScopedFilesystem, ZombieRole,
 };
@@ -40,17 +40,14 @@ impl<T: FileSystem> std::fmt::Debug for Network<T> {
     }
 }
 
-#[derive(Default, Debug, Clone)]
-pub struct AddNodeOpts {
-    pub image: Option<Image>,
-    pub command: Option<Command>,
-    pub args: Vec<Arg>,
-    pub env: Vec<EnvVar>,
-    pub is_validator: bool,
-    pub rpc_port: Option<Port>,
-    pub prometheus_port: Option<Port>,
-    pub p2p_port: Option<Port>,
-}
+macros::create_add_options!(AddNodeOptions {
+    chain_spec: Option<PathBuf>
+});
+
+macros::create_add_options!(AddCollatorOptions {
+    chain_spec: Option<PathBuf>,
+    chain_spec_relay: Option<PathBuf>
+});
 
 impl<T: FileSystem> Network<T> {
     pub(crate) fn new_with_relay(
@@ -69,108 +66,225 @@ impl<T: FileSystem> Network<T> {
         }
     }
 
-    // Pub API
+    // Pubic API
+
+    pub fn relaychain(&self) -> &Relaychain {
+        &self.relay
+    }
 
     // Teardown the network
     // destroy()
 
+    /// Add a node to the relaychain
+    ///
+    /// NOTE: name must be unique in the whole network. The new node is added to the
+    /// running network instance.
+    ///
+    /// # Example:
+    /// ```rust
+    /// # use provider::NativeProvider;
+    /// # use support::{fs::local::LocalFileSystem, process::os::OsProcessManager};
+    /// # use zombienet_orchestrator::{errors, AddNodeOptions, Orchestrator};
+    /// # use configuration::NetworkConfig;
+    /// # async fn example() -> Result<(), errors::OrchestratorError> {
+    /// #   let provider = NativeProvider::new(LocalFileSystem {}, OsProcessManager {});
+    /// #   let orchestrator = Orchestrator::new(LocalFileSystem {}, provider);
+    /// #   let config = NetworkConfig::load_from_toml("config.toml")?;
+    /// let mut network = orchestrator.spawn(config).await?;
+    ///
+    /// // Create the options to add the new node
+    /// let opts = AddNodeOptions {
+    ///     rpc_port: Some(9444),
+    ///     is_validator: true,
+    ///     ..Default::default()
+    /// };
+    ///
+    /// network.add_node("new-node", opts).await?;
+    /// #   Ok(())
+    /// # }
+    /// ```
     pub async fn add_node(
         &mut self,
         name: impl Into<String>,
-        options: AddNodeOpts,
-        para_id: Option<u32>,
+        options: AddNodeOptions,
     ) -> Result<(), anyhow::Error> {
-        // build context
-        // let (maybe_para_chain_id, chain_context, para_spec, role) =
-        let (chain_context, role, maybe_para_chain_id, para_spec, maybe_para_chain_spec_path) =
-            if let Some(para_id) = para_id {
-                let spec = self
-                    .initial_spec
-                    .parachains
-                    .iter()
-                    .find(|para| para.id == para_id)
-                    .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?;
-                let role = if spec.is_cumulus_based {
-                    ZombieRole::CumulusCollator
-                } else {
-                    ZombieRole::Collator
-                };
-                let chain_context = ChainDefaultContext {
-                    default_command: spec.default_command.as_ref(),
-                    default_image: spec.default_image.as_ref(),
-                    default_resources: spec.default_resources.as_ref(),
-                    default_db_snapshot: spec.default_db_snapshot.as_ref(),
-                    default_args: spec.default_args.iter().collect(),
-                };
-                let parachain = self
-                    .parachains
-                    .get(&para_id)
-                    .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?;
-
-                // (parachain.chain_id.clone(), chain_context, Some(spec), role)
-                (
-                    chain_context,
-                    role,
-                    parachain.chain_id.clone(),
-                    Some(spec),
-                    parachain.chain_spec_path.clone(),
-                )
-            } else {
-                let spec = &self.initial_spec.relaychain;
-                let chain_context = ChainDefaultContext {
-                    default_command: spec.default_command.as_ref(),
-                    default_image: spec.default_image.as_ref(),
-                    default_resources: spec.default_resources.as_ref(),
-                    default_db_snapshot: spec.default_db_snapshot.as_ref(),
-                    default_args: spec.default_args.iter().collect(),
-                };
-                (chain_context, ZombieRole::Node, None, None, None)
-            };
+        let name = name.into();
+        let relaychain = self.relaychain();
+
+        if self.nodes_by_name.contains_key(&name) {
+            return Err(anyhow::anyhow!("Name: {} is already used.", name));
+        }
+
+        let chain_spec_path = if let Some(chain_spec_custom_path) = &options.chain_spec {
+            chain_spec_custom_path.clone()
+        } else {
+            PathBuf::from(format!(
+                "{}/{}.json",
+                self.ns.base_dir().to_string_lossy(),
+                relaychain.chain
+            ))
+        };
+
+        let chain_context = ChainDefaultContext {
+            default_command: self.initial_spec.relaychain.default_command.as_ref(),
+            default_image: self.initial_spec.relaychain.default_image.as_ref(),
+            default_resources: self.initial_spec.relaychain.default_resources.as_ref(),
+            default_db_snapshot: self.initial_spec.relaychain.default_db_snapshot.as_ref(),
+            default_args: self.initial_spec.relaychain.default_args.iter().collect(),
+        };
 
         let node_spec =
-            network_spec::node::NodeSpec::from_ad_hoc(name.into(), options, &chain_context)?;
+            network_spec::node::NodeSpec::from_ad_hoc(&name, options.into(), &chain_context)?;
+        let base_dir = self.ns.base_dir().to_string_lossy();
+        let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
+
+        let ctx = SpawnNodeCtx {
+            chain_id: &relaychain.chain_id,
+            parachain_id: None,
+            chain: &relaychain.chain,
+            role: ZombieRole::Node,
+            ns: &self.ns,
+            scoped_fs: &scoped_fs,
+            parachain: None,
+            bootnodes_addr: &vec![],
+            wait_ready: true,
+        };
+
+        let global_files_to_inject = vec![TransferedFile {
+            local_path: chain_spec_path,
+            remote_path: PathBuf::from(format!("/cfg/{}.json", relaychain.chain)),
+        }];
+
+        let node = spawner::spawn_node(&node_spec, global_files_to_inject, &ctx).await?;
+
+        // TODO: register the new node as validator in the relaychain
+        // STEPS:
+        //  - check balance of `stash` derivation for validator account
+        //  - call rotate_keys on the new validator
+        //  - call setKeys on the new validator
+        // if node_spec.is_validator {
+        //     let running_node = self.relay.nodes.first().unwrap();
+        //     // tx_helper::validator_actions::register(vec![&node], &running_node.ws_uri, None).await?;
+        // }
+
+        // Add node to the global hash
+        self.add_running_node(node.clone(), None);
+        // add node to relay
+        self.relay.nodes.push(node);
+
+        Ok(())
+    }
+
+    /// Add a new collator to a parachain
+    ///
+    /// NOTE: name must be unique in the whole network.
+    ///
+    /// # Example:
+    /// ```rust
+    /// # use provider::NativeProvider;
+    /// # use support::{fs::local::LocalFileSystem, process::os::OsProcessManager};
+    /// # use zombienet_orchestrator::{errors, AddCollatorOptions, Orchestrator};
+    /// # use configuration::NetworkConfig;
+    /// # async fn example() -> Result<(), anyhow::Error> {
+    /// #   let provider = NativeProvider::new(LocalFileSystem {}, OsProcessManager {});
+    /// #   let orchestrator = Orchestrator::new(LocalFileSystem {}, provider);
+    /// #   let config = NetworkConfig::load_from_toml("config.toml")?;
+    /// let mut network = orchestrator.spawn(config).await?;
+    ///
+    /// let col_opts = AddCollatorOptions {
+    ///     command: Some("polkadot-parachain".try_into()?),
+    ///     ..Default::default()
+    /// };
+    ///
+    /// network.add_collator("new-col-1", col_opts, 100).await?;
+    /// #   Ok(())
+    /// # }
+    /// ```
+    pub async fn add_collator(
+        &mut self,
+        name: impl Into<String>,
+        options: AddCollatorOptions,
+        para_id: u32,
+    ) -> Result<(), anyhow::Error> {
+        let spec = self
+            .initial_spec
+            .parachains
+            .iter()
+            .find(|para| para.id == para_id)
+            .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?;
+        let role = if spec.is_cumulus_based {
+            ZombieRole::CumulusCollator
+        } else {
+            ZombieRole::Collator
+        };
+        let chain_context = ChainDefaultContext {
+            default_command: spec.default_command.as_ref(),
+            default_image: spec.default_image.as_ref(),
+            default_resources: spec.default_resources.as_ref(),
+            default_db_snapshot: spec.default_db_snapshot.as_ref(),
+            default_args: spec.default_args.iter().collect(),
+        };
+        let parachain = self
+            .parachains
+            .get(&para_id)
+            .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?;
+
         let base_dir = self.ns.base_dir().to_string_lossy();
         let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
 
         // TODO: we want to still supporting spawn a dedicated bootnode??
         let ctx = SpawnNodeCtx {
             chain_id: &self.relay.chain_id,
-            parachain_id: maybe_para_chain_id.as_deref(),
+            parachain_id: parachain.chain_id.as_deref(),
             chain: &self.relay.chain,
             role,
             ns: &self.ns,
             scoped_fs: &scoped_fs,
-            parachain: para_spec,
+            parachain: Some(spec),
             bootnodes_addr: &vec![],
             wait_ready: true,
         };
 
-        let mut global_files_to_inject = vec![TransferedFile {
-            local_path: PathBuf::from(format!(
+        let relaychain_spec_path = if let Some(chain_spec_custom_path) = &options.chain_spec_relay {
+            chain_spec_custom_path.clone()
+        } else {
+            PathBuf::from(format!(
                 "{}/{}.json",
                 self.ns.base_dir().to_string_lossy(),
                 self.relay.chain
-            )),
+            ))
+        };
+
+        let mut global_files_to_inject = vec![TransferedFile {
+            local_path: relaychain_spec_path,
             remote_path: PathBuf::from(format!("/cfg/{}.json", self.relay.chain)),
         }];
 
-        if let Some(para_spec_path) = maybe_para_chain_spec_path {
+        let para_chain_spec_local_path = if let Some(para_chain_spec_custom) = &options.chain_spec {
+            Some(para_chain_spec_custom.clone())
+        } else if let Some(para_spec_path) = &parachain.chain_spec_path {
+            Some(PathBuf::from(format!(
+                "{}/{}",
+                self.ns.base_dir().to_string_lossy(),
+                para_spec_path.to_string_lossy()
+            )))
+        } else {
+            None
+        };
+
+        if let Some(para_spec_path) = para_chain_spec_local_path {
             global_files_to_inject.push(TransferedFile {
-                local_path: PathBuf::from(format!(
-                    "{}/{}",
-                    self.ns.base_dir().to_string_lossy(),
-                    para_spec_path.to_string_lossy()
-                )),
-                remote_path: PathBuf::from(format!(
-                    "/cfg/{}.json",
-                    para_id.ok_or(anyhow::anyhow!(
-                        "para_id should be valid here, this is a bug!"
-                    ))?
-                )),
+                local_path: para_spec_path,
+                remote_path: PathBuf::from(format!("/cfg/{}.json", para_id)),
             });
         }
 
+        let node_spec =
+            network_spec::node::NodeSpec::from_ad_hoc(name.into(), options.into(), &chain_context)?;
+
         let node = spawner::spawn_node(&node_spec, global_files_to_inject, &ctx).await?;
+        let para = self.parachains.get_mut(&para_id).unwrap();
+        para.collators.push(node.clone());
         self.add_running_node(node, None);
 
         Ok(())
@@ -228,10 +342,6 @@ impl<T: FileSystem> Network<T> {
         self.ns.name()
     }
 
-    pub(crate) fn relaychain(&self) -> &Relaychain {
-        &self.relay
-    }
-
     pub(crate) fn parachain(&self, para_id: u32) -> Option<&Parachain> {
         self.parachains.get(&para_id)
     }
diff --git a/crates/orchestrator/src/network/node.rs b/crates/orchestrator/src/network/node.rs
index 904f96f1ecc9c1e99a79437c6f8e37598994a2e2..63ffaf36f80f67e7d941e59e5baddccec2043f02 100644
--- a/crates/orchestrator/src/network/node.rs
+++ b/crates/orchestrator/src/network/node.rs
@@ -39,6 +39,14 @@ impl NetworkNode {
         }
     }
 
+    pub fn spec(&self) -> &NodeSpec {
+        &self.spec
+    }
+
+    pub fn ws_uri(&self) -> &str {
+        &self.ws_uri
+    }
+
     /// Pause the node, this is implemented by pausing the
     /// actual process (e.g polkadot) with sendig `SIGSTOP` signal
     pub async fn pause(&self) -> Result<(), anyhow::Error> {
diff --git a/crates/orchestrator/src/network/relaychain.rs b/crates/orchestrator/src/network/relaychain.rs
index 74886165298f408d616e410e8b004609f8dc52d8..356f84c2a060f06b1c4ad0792260ebad7ee165e4 100644
--- a/crates/orchestrator/src/network/relaychain.rs
+++ b/crates/orchestrator/src/network/relaychain.rs
@@ -19,4 +19,9 @@ impl Relaychain {
             nodes: Default::default(),
         }
     }
+
+    // Public API
+    pub fn chain(&self) -> &str {
+        &self.chain
+    }
 }
diff --git a/crates/orchestrator/src/network_spec/node.rs b/crates/orchestrator/src/network_spec/node.rs
index 15d9580b250590478cb61b354d45f35cdab3452f..b49885e521c319b122b1594f89b9bebcab5515ec 100644
--- a/crates/orchestrator/src/network_spec/node.rs
+++ b/crates/orchestrator/src/network_spec/node.rs
@@ -4,14 +4,43 @@ use configuration::shared::{
     types::{Arg, AssetLocation, Command, Image},
 };
 use multiaddr::Multiaddr;
+use provider::types::Port;
 
 use crate::{
     errors::OrchestratorError,
     generators,
-    network::AddNodeOpts,
-    shared::types::{ChainDefaultContext, NodeAccounts, ParkedPort},
+    network::AddNodeOptions,
+    shared::{
+        macros,
+        types::{ChainDefaultContext, NodeAccounts, ParkedPort},
+    },
+    AddCollatorOptions,
 };
 
+macros::create_add_options!(AddNodeSpecOpts {});
+
+macro_rules! impl_from_for_add_node_opts {
+    ($struct:ident) => {
+        impl From<$struct> for AddNodeSpecOpts {
+            fn from(value: $struct) -> Self {
+                Self {
+                    image: value.image,
+                    command: value.command,
+                    args: value.args,
+                    env: value.env,
+                    is_validator: value.is_validator,
+                    rpc_port: value.rpc_port,
+                    prometheus_port: value.prometheus_port,
+                    p2p_port: value.p2p_port,
+                }
+            }
+        }
+    };
+}
+
+impl_from_for_add_node_opts!(AddNodeOptions);
+impl_from_for_add_node_opts!(AddCollatorOptions);
+
 /// A node configuration, with fine-grained configuration options.
 #[derive(Debug, Clone)]
 pub struct NodeSpec {
@@ -145,7 +174,7 @@ impl NodeSpec {
 
     pub fn from_ad_hoc(
         name: impl Into<String>,
-        options: AddNodeOpts,
+        options: AddNodeSpecOpts,
         chain_context: &ChainDefaultContext,
     ) -> Result<Self, OrchestratorError> {
         // Check first if the image is set at node level, then try with the default
diff --git a/crates/orchestrator/src/shared.rs b/crates/orchestrator/src/shared.rs
index cd408564ea089141dd830d4524e027b1064b33d7..9bedb0e5fabb56ff7c5b852f78cbfa517a932ef4 100644
--- a/crates/orchestrator/src/shared.rs
+++ b/crates/orchestrator/src/shared.rs
@@ -1 +1,2 @@
+pub mod macros;
 pub mod types;
diff --git a/crates/orchestrator/src/shared/macros.rs b/crates/orchestrator/src/shared/macros.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f79b3a1aa6ecb225605ee147725c69fd6836ad30
--- /dev/null
+++ b/crates/orchestrator/src/shared/macros.rs
@@ -0,0 +1,30 @@
+macro_rules! create_add_options {
+    ($struct:ident {$( $field:ident:$type:ty ),*}) =>{
+        #[derive(Default, Debug, Clone)]
+        pub struct $struct {
+            /// Image to run the node
+            pub image: Option<Image>,
+            /// Command to run the node
+            pub command: Option<Command>,
+            /// Arguments to pass to the node
+            pub args: Vec<Arg>,
+            /// Env vars to set
+            pub env: Vec<EnvVar>,
+            /// Make the node a validator
+            ///
+            /// This implies `--validator` or `--collator`
+            pub is_validator: bool,
+            /// RPC port to use, if None a random one will be set
+            pub rpc_port: Option<Port>,
+            /// Prometheus port to use, if None a random one will be set
+            pub prometheus_port: Option<Port>,
+            /// P2P port to use, if None a random one will be set
+            pub p2p_port: Option<Port>,
+            $(
+                pub $field: $type,
+            )*
+        }
+    };
+}
+
+pub(crate) use create_add_options;
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
index f113b2a15e47acd991b115116dd16c06b2a7c5e5..867e483f84195ee2d80085c429573ab154ec0ae1 100644
--- a/crates/sdk/src/lib.rs
+++ b/crates/sdk/src/lib.rs
@@ -1,6 +1,8 @@
 use async_trait::async_trait;
 pub use configuration::{NetworkConfig, NetworkConfigBuilder, RegistrationStrategy};
-pub use orchestrator::{errors::OrchestratorError, network::Network, AddNodeOpts, Orchestrator};
+pub use orchestrator::{
+    errors::OrchestratorError, network::Network, AddCollatorOptions, AddNodeOptions, Orchestrator,
+};
 use provider::NativeProvider;
 use support::{fs::local::LocalFileSystem, process::os::OsProcessManager};