diff --git a/gso/services/provisioning_proxy.py b/gso/services/provisioning_proxy.py
index c76900e109a93586f3c262322db734c32dfe7cae..4c90ea7b894fb8b58c0d08604e8197aa2d6b770b 100644
--- a/gso/services/provisioning_proxy.py
+++ b/gso/services/provisioning_proxy.py
@@ -19,12 +19,11 @@ from pydantic import validator
 
 from gso import settings
 from gso.products.product_types.iptrunk import Iptrunk, IptrunkProvisioning
-from gso.products.product_types.router import RouterProvisioning
+from gso.products.product_types.router import Router, RouterProvisioning
 
 logger = logging.getLogger(__name__)
-"""{class}`logging.Logger` instance."""
 DEFAULT_LABEL = "Provisioning proxy is running. Please come back later for the results."
-"""The default label displayed when the provisioning proxy is running."""
+"""The default label displayed when the provisioning proxy is running, in case no custom label is provided."""
 
 
 class CUDOperation(strEnum):
@@ -159,6 +158,52 @@ def deprovision_ip_trunk(subscription: Iptrunk, process_id: UUIDstr, dry_run: bo
     _send_request("ip_trunk", parameters, process_id, CUDOperation.DELETE)
 
 
+def migrate_ip_trunk(
+    subscription: Iptrunk,
+    new_node: Router,
+    new_lag_interface: str,
+    new_lag_member_interfaces: list[str],
+    replace_index: int,
+    process_id: UUIDstr,
+    verb: str,
+    dry_run: bool = True,
+) -> None:
+    """Migrate an IP trunk service using {term}`LSO`.
+
+    :param subscription: The subscription object that's to be migrated.
+    :type subscription: {class}`Iptrunk`
+    :param new_node: The new node that is being migrated to
+    :type new_node: {class}`Router`
+    :param new_lag_interface: The name of the new aggregated Ethernet interface
+    :type new_lag_interface: str
+    :param new_lag_member_interfaces: The new list of interfaces that are part of the LAG
+    :type new_lag_member_interfaces: list[str]
+    :param replace_index: The index of the side that is going to be replaced as part of the existing trunk,
+                          can be `0` or `1`.
+    :type replace_index: int
+    :param process_id: The related process ID, used for callback.
+    :type process_id: UUIDstr
+    :param verb: The verb that is passed to the executed playbook
+    :type verb: str
+    :param dry_run: A boolean indicating whether this should be a dry run or not, defaults to `True`.
+    :type dry_run: bool
+    :rtype: None
+    """
+    parameters = {
+        "subscription": json.loads(json_dumps(subscription)),
+        "new_side": {
+            "new_node": json.loads(json_dumps(new_node)),
+            "new_lag_interface": new_lag_interface,
+            "new_lag_member_interfaces": new_lag_member_interfaces,
+            "replace_index": replace_index,
+        },
+        "verb": verb,
+        "dry_run": dry_run,
+    }
+
+    _send_request("ip_trunk/migrate", parameters, process_id, CUDOperation.POST)
+
+
 @inputstep("Await provisioning proxy results", assignee=Assignee("SYSTEM"))
 def _await_pp_results(subscription: SubscriptionModel, label_text: str = DEFAULT_LABEL) -> FormGenerator:
     """Input step that forces the workflow to go into a `SUSPENDED` state.
diff --git a/gso/workflows/iptrunk/__init__.py b/gso/workflows/iptrunk/__init__.py
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ee4b7535aa2257e514bb023df427d9ae5fb80fd0 100644
--- a/gso/workflows/iptrunk/__init__.py
+++ b/gso/workflows/iptrunk/__init__.py
@@ -0,0 +1,24 @@
+from logging import getLogger
+
+from orchestrator import step
+from orchestrator.types import State
+from products import Iptrunk
+
+logger = getLogger(__name__)
+
+
+@step("Set ISIS metric to 9000")
+def set_isis_to_9000(subscription: Iptrunk) -> State:
+    old_isis_metric = subscription.iptrunk.iptrunk_isis_metric
+    subscription.iptrunk.iptrunk_isis_metric = 90000
+    logger.warning("ISIS metric is only updated in the subscription, not in the real world.")
+
+    return {"subscription": subscription, "old_isis_metric": old_isis_metric}
+
+
+@step("Restore ISIS metric to original value")
+def restore_isis_metric(subscription: Iptrunk, old_isis_metric: int) -> State:
+    subscription.iptrunk.iptrunk_isis_metric = old_isis_metric
+    logger.warning("ISIS metric is only updated in the subscription, not in the real world.")
+
+    return {"subscription": subscription}
diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py
index 147377a3501a8656f8cb50e6c649e44bf1566ced..0bc9d4aedf5e4b36dabf0faf132b22cb75a0bb6a 100644
--- a/gso/workflows/iptrunk/create_iptrunk.py
+++ b/gso/workflows/iptrunk/create_iptrunk.py
@@ -1,5 +1,3 @@
-from uuid import UUID
-
 from orchestrator.db.models import ProductTable, SubscriptionTable
 from orchestrator.forms import FormPage
 from orchestrator.forms.validators import Choice, UniqueConstrainedList
@@ -10,7 +8,7 @@ from orchestrator.workflows.steps import resync, set_status, store_process_subsc
 from orchestrator.workflows.utils import wrap_create_initial_input_form
 
 from gso.products.product_blocks import PhyPortCapacity
-from gso.products.product_blocks.iptrunk import IptrunkType, IptrunkSideBlock
+from gso.products.product_blocks.iptrunk import IptrunkType
 from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning
 from gso.products.product_types.router import Router
 from gso.services import ipam, provisioning_proxy, subscriptions
diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py
index c1a86049d947f2b7430a42e975311b11d2e3ddd6..ab61c1cebfd31e38739d3d29d5b60dc67c411af4 100644
--- a/gso/workflows/iptrunk/migrate_iptrunk.py
+++ b/gso/workflows/iptrunk/migrate_iptrunk.py
@@ -1,18 +1,24 @@
 import re
+from logging import getLogger
 from typing import NoReturn
 
 from orchestrator import step, workflow
+from orchestrator.config.assignee import Assignee
 from orchestrator.db import ProductTable, SubscriptionTable
 from orchestrator.forms import FormPage
-from orchestrator.forms.validators import Choice, UniqueConstrainedList
+from orchestrator.forms.validators import Choice, Label, UniqueConstrainedList
 from orchestrator.targets import Target
 from orchestrator.types import FormGenerator, State, UUIDstr
-from orchestrator.workflow import StepList, done, init
+from orchestrator.workflow import StepList, done, init, inputstep
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from products import Iptrunk, Router
 from pydantic import validator
+from services import provisioning_proxy
+from services.provisioning_proxy import pp_interaction
+from workflows.iptrunk import restore_isis_metric, set_isis_to_9000
 
-from products import Iptrunk
+logger = getLogger(__name__)
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
@@ -35,15 +41,10 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
             routers[str(router_id)] = router_description
 
     NewRouterEnum = Choice("Select a new router", zip(routers.keys(), routers.items()))  # type: ignore
-    ReplacedSide = Choice(
-            "Select the side of the IP trunk to be replaced",
-            [  # type: ignore
-                (str(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.subscription.subscription_id),
-                 subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.subscription.description),
-                (str(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.subscription.subscription_id),
-                 subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.subscription.description),
-            ],
-        )
+    sides_dict = {
+        side.subscription.subscription_id: side.subscription.description for side in subscription.iptrunk.iptrunk_sides
+    }
+    ReplacedSide = Choice("Select the side of the IP trunk to be replaced", zip(sides_dict.keys(), sides_dict.items()))
 
     class LagMemberList(UniqueConstrainedList[str]):
         min_items = len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members)
@@ -74,12 +75,285 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
     user_input = yield ModifyIptrunkForm
 
-    return user_input.dict()
+    def _find_updated_side_of_trunk(trunk: Iptrunk, new_side: str) -> int:
+        sides = trunk.iptrunk.iptrunk_sides
+        if str(sides[0].iptrunk_side_node.subscription.subscription_id) == new_side:
+            return 0
+        elif str(sides[1].iptrunk_side_node.subscription.subscription_id) == new_side:
+            return 1
+        raise ValueError("Invalid Router id provided to be replaced!")
+
+    replace_index = _find_updated_side_of_trunk(subscription, user_input["replace_side"])
+
+    return user_input.dict() | {"replace_index": replace_index}
+
+
+@inputstep("Wait for confirmation", assignee=Assignee.SYSTEM)
+def confirm_continue() -> State:
+    class ProvisioningResultPage(FormPage):
+        class Config:
+            title = "Please confirm before continuing"
+
+        info_label: Label = (  # type: ignore
+            "ISIS metric has been set to 9000, please confirm to continue the workflow when ready."
+        )
+
+    yield ProvisioningResultPage
+
+    return {}
+
+
+@step("[DRY RUN] Disable configuration on old router")
+def disable_old_config_dry(
+    subscription: Iptrunk,
+    new_node: Router,
+    new_lag_interface: str,
+    new_lag_member_interfaces: list[str],
+    replace_index: int,
+    process_id: UUIDstr,
+) -> State:
+    provisioning_proxy.migrate_ip_trunk(
+        subscription,
+        new_node,
+        new_lag_interface,
+        new_lag_member_interfaces,
+        replace_index,
+        process_id,
+        "VERB NEEDS TO BE UPDATED",
+    )
+
+    return {
+        "subscription": subscription,
+        "label_text": "[DRY RUN] Migrating old trunk interface, please refresh to get the results of the playbook.",
+    }
+
+
+@step("[REAL] Disable configuration on old router")
+def disable_old_config_real(
+    subscription: Iptrunk,
+    new_node: Router,
+    new_lag_interface: str,
+    new_lag_member_interfaces: list[str],
+    replace_index: int,
+    process_id: UUIDstr,
+) -> State:
+    provisioning_proxy.migrate_ip_trunk(
+        subscription,
+        new_node,
+        new_lag_interface,
+        new_lag_member_interfaces,
+        replace_index,
+        process_id,
+        "VERB NEEDS TO BE UPDATED",
+        False,
+    )
+
+    logger.warning("Playbook verb is not yet properly set.")
+
+    return {
+        "subscription": subscription,
+        "label_text": "Migrating old trunk interface, please refresh to get the results of the playbook.",
+    }
+
+
+@step("[DRY RUN] Deploy configuration on new router")
+def deploy_new_config_dry(
+    subscription: Iptrunk,
+    new_node: Router,
+    new_lag_interface: str,
+    new_lag_member_interfaces: list[str],
+    replace_index: int,
+    process_id: UUIDstr,
+) -> State:
+    provisioning_proxy.migrate_ip_trunk(
+        subscription,
+        new_node,
+        new_lag_interface,
+        new_lag_member_interfaces,
+        replace_index,
+        process_id,
+        "VERB NEEDS TO BE UPDATED",
+    )
+
+    logger.warning("Playbook verb is not yet properly set.")
+
+    return {
+        "subscription": subscription,
+        "label_text": "[DRY RUN] Deploying new trunk interface, please refresh to get the results of the playbook.",
+    }
+
+
+@step("Deploy configuration on new router")
+def deploy_new_config_real(
+    subscription: Iptrunk,
+    new_node: Router,
+    new_lag_interface: str,
+    new_lag_member_interfaces: list[str],
+    replace_index: int,
+    process_id: UUIDstr,
+) -> State:
+    provisioning_proxy.migrate_ip_trunk(
+        subscription,
+        new_node,
+        new_lag_interface,
+        new_lag_member_interfaces,
+        replace_index,
+        process_id,
+        "VERB NEEDS TO BE UPDATED",
+        False,
+    )
+
+    logger.warning("Playbook verb is not yet properly set.")
+
+    return {
+        "subscription": subscription,
+        "label_text": "Deploying new trunk interface, please refresh to get the results of the playbook.",
+    }
+
+
+@step("Run interface checks")
+def run_interface_checks(
+    subscription: Iptrunk,
+    new_node: Router,
+    new_lag_interface: str,
+    new_lag_member_interfaces: list[str],
+    replace_index: int,
+    process_id: UUIDstr,
+) -> State:
+    provisioning_proxy.migrate_ip_trunk(
+        subscription,
+        new_node,
+        new_lag_interface,
+        new_lag_member_interfaces,
+        replace_index,
+        process_id,
+        "MIGRATION_INTERFACE_CHECK",
+    )
+
+    logger.warning("Playbook verb is not yet properly set.")
+
+    return {
+        "subscription": subscription,
+        "label_text": "Running checks on the new trunk interface, please refresh to get the results of the playbook.",
+    }
+
+
+@step("Deploy configuration on new router")
+def deploy_new_isis(
+    subscription: Iptrunk,
+    new_node: Router,
+    new_lag_interface: str,
+    new_lag_member_interfaces: list[str],
+    replace_index: int,
+    process_id: UUIDstr,
+) -> State:
+    provisioning_proxy.migrate_ip_trunk(
+        subscription,
+        new_node,
+        new_lag_interface,
+        new_lag_member_interfaces,
+        replace_index,
+        process_id,
+        "VERB NEEDS TO BE UPDATED",
+        False,
+    )
+
+    logger.warning("Playbook verb is not yet properly set.")
+
+    return {
+        "subscription": subscription,
+        "label_text": "Updating new ISIS metric, please refresh to get the results of the playbook.",
+    }
+
+
+@step("Check ISIS metric")
+def check_isis(subscription: Iptrunk, process_id: UUIDstr) -> State:
+    provisioning_proxy.check_ip_trunk(subscription, process_id, "VERB NEEDS TO BE UPDATED")
+
+    logger.warning("Playbook verb is not yet properly set.")
+
+    return {
+        "subscription": subscription,
+        "label_text": "Checking ISIS functionality, please refresh to get the results of the playbook.",
+    }
+
+
+@step("[DRY RUN] Delete configuration on old router")
+def delete_old_config_dry(
+    subscription: Iptrunk,
+    new_node: Router,
+    new_lag_interface: str,
+    new_lag_member_interfaces: list[str],
+    replace_index: int,
+    process_id: UUIDstr,
+) -> State:
+    provisioning_proxy.migrate_ip_trunk(
+        subscription,
+        new_node,
+        new_lag_interface,
+        new_lag_member_interfaces,
+        replace_index,
+        process_id,
+        "VERB NEEDS TO BE UPDATED",
+    )
+
+    logger.warning("Playbook verb is not yet properly set.")
+
+    return {
+        "subscription": subscription,
+        "label_text": "[DRY RUN] Removing configuration from old router,"
+        "please refresh to get the results of the playbook.",
+    }
+
+
+@step("Delete configuration on old router")
+def delete_old_config_real(
+    subscription: Iptrunk,
+    new_node: Router,
+    new_lag_interface: str,
+    new_lag_member_interfaces: list[str],
+    replace_index: int,
+    process_id: UUIDstr,
+) -> State:
+    provisioning_proxy.migrate_ip_trunk(
+        subscription,
+        new_node,
+        new_lag_interface,
+        new_lag_member_interfaces,
+        replace_index,
+        process_id,
+        "VERB NEEDS TO BE UPDATED",
+        False,
+    )
+
+    logger.warning("Playbook verb is not yet properly set.")
+
+    return {
+        "subscription": subscription,
+        "label_text": "Removing configuration from old router, please refresh to get the results of the playbook.",
+    }
+
+
+@step("Update IPAM")
+def update_ipam(subscription: Iptrunk) -> State:
+    pass
+
+    return {"subscription": subscription}
+
 
+@step("Update subscription model")
+def update_subscription_model(
+    subscription: Iptrunk,
+    replace_index: int,
+    new_node: UUIDstr,
+    new_lag_interface: str,
+    new_lag_member_interfaces: list[str],
+) -> State:
+    subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_node = Router.from_subscription(new_node).router
+    subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_iface = new_lag_interface
+    subscription.iptrunk.iptrunk_sides[replace_index].iptrunk_side_ae_members = new_lag_member_interfaces
 
-@step("Hilversum Stinks")
-def temp_test_step(subscription: Iptrunk) -> State:
-    return {"test": subscription}
+    return {"subscription": subscription}
 
 
 @workflow(
@@ -89,14 +363,25 @@ def temp_test_step(subscription: Iptrunk) -> State:
 )
 def migrate_iptrunk() -> StepList:
     return (
-            init
-            >> store_process_subscription(Target.MODIFY)
-            >> unsync
-            >> temp_test_step
-            # >> set ISIS to 9000
-            # >> wait confirm
-            # >> disable config
-            # >>
-            >> resync
-            >> done
+        init
+        >> store_process_subscription(Target.MODIFY)
+        >> unsync
+        >> set_isis_to_9000
+        >> confirm_continue
+        >> pp_interaction(disable_old_config_dry, 3)
+        >> pp_interaction(disable_old_config_real, 3)
+        >> pp_interaction(deploy_new_config_dry, 3)
+        >> pp_interaction(deploy_new_config_real, 3)
+        >> confirm_continue
+        >> pp_interaction(run_interface_checks, 3)
+        >> pp_interaction(deploy_new_isis, 3)
+        >> pp_interaction(check_isis, 3)
+        >> confirm_continue
+        >> restore_isis_metric
+        >> pp_interaction(delete_old_config_dry, 3)
+        >> pp_interaction(delete_old_config_real, 3)
+        >> update_ipam
+        >> update_subscription_model
+        >> resync
+        >> done
     )
diff --git a/gso/workflows/iptrunk/terminate_iptrunk.py b/gso/workflows/iptrunk/terminate_iptrunk.py
index 97659572218f5e11b6a59f16aa1239d1283d809d..4572bb650bd93d628d329844d9d816411daeb3a1 100644
--- a/gso/workflows/iptrunk/terminate_iptrunk.py
+++ b/gso/workflows/iptrunk/terminate_iptrunk.py
@@ -7,6 +7,7 @@ from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUID
 from orchestrator.workflow import StepList, conditional, done, init, step, workflow
 from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from workflows.iptrunk import set_isis_to_9000
 
 from gso.products.product_types.iptrunk import Iptrunk
 from gso.services import ipam, provisioning_proxy
@@ -27,22 +28,13 @@ def initial_input_form_generator() -> FormGenerator:
     return user_input.dict()
 
 
-@step("Set iptrunk ISIS metric to 9000")
-def update_isis_metric(subscription: Iptrunk) -> State:
-    subscription.iptrunk.iptrunk_isis_metric = 9000
-
-    return {"subscription": subscription}
-
-
 @step("Drain traffic from trunk")
 def drain_traffic_from_ip_trunk(subscription: Iptrunk, process_id: UUIDstr) -> State:
     provisioning_proxy.provision_ip_trunk(subscription, process_id, "isis_interface", False)
     return {
         "subscription": subscription,
-        "label_text": "This is setting the ISIS metric of the trunk to 9000"
-        "trunk. "
-        "Press refresh to get the results\n"
-        "When traffic is drained, confirm to continue",
+        "label_text": "This is setting the ISIS metric of the trunk to 9000. Press refresh to get the results."
+        "When traffic is drained, confirm to continue.",
     }
 
 
@@ -94,7 +86,7 @@ def terminate_iptrunk() -> StepList:
     run_ipam_steps = conditional(lambda state: state.get("clean_up_ipam", True))
 
     config_steps = (
-        StepList([update_isis_metric])
+        StepList([set_isis_to_9000])
         >> pp_interaction(drain_traffic_from_ip_trunk, 3)
         >> pp_interaction(deprovision_ip_trunk_dry, 3)
         >> pp_interaction(deprovision_ip_trunk_real, 3)