diff --git a/gso/migrations/versions/2024-08-07_88dd5a44150d_add_promote_p_to_pe_workflows.py b/gso/migrations/versions/2024-08-07_88dd5a44150d_add_promote_p_to_pe_workflows.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ad1235c2c676c1c7164647d5fb8b483247ea919
--- /dev/null
+++ b/gso/migrations/versions/2024-08-07_88dd5a44150d_add_promote_p_to_pe_workflows.py
@@ -0,0 +1,39 @@
+"""Add promote P to PE  workflows..
+
+Revision ID: 88dd5a44150d
+Revises: 41fd1ae225aq
+Create Date: 2024-08-07 13:54:44.362435
+
+"""
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = '88dd5a44150d'
+down_revision = '41fd1ae225aq'
+branch_labels = None
+depends_on = None
+
+
+from orchestrator.migrations.helpers import create_workflow, delete_workflow
+
+new_workflows = [
+    {
+        "name": "promote_p_to_pe",
+        "target": "MODIFY",
+        "description": "Promote P router to PE router",
+        "product_type": "Router"
+    }
+]
+
+
+def upgrade() -> None:
+    conn = op.get_bind()
+    for workflow in new_workflows:
+        create_workflow(conn, workflow)
+
+
+def downgrade() -> None:
+    conn = op.get_bind()
+    for workflow in new_workflows:
+        delete_workflow(conn, workflow["name"])
diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index 56561838557882cac784faed6745c1d80661a0cd..e788472232d4625afa14ae9fbfb932cd3573b012 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -44,6 +44,7 @@ LazyWorkflowInstance("gso.workflows.router.modify_connection_strategy", "modify_
 LazyWorkflowInstance("gso.workflows.router.import_router", "import_router")
 LazyWorkflowInstance("gso.workflows.router.create_imported_router", "create_imported_router")
 LazyWorkflowInstance("gso.workflows.router.validate_router", "validate_router")
+LazyWorkflowInstance("gso.workflows.router.promote_p_to_pe", "promote_p_to_pe")
 
 #  Site workflows
 LazyWorkflowInstance("gso.workflows.site.create_site", "create_site")
diff --git a/gso/workflows/router/promote_p_to_pe.py b/gso/workflows/router/promote_p_to_pe.py
new file mode 100644
index 0000000000000000000000000000000000000000..d893c057167a20a7971adccdfa6d9a374b4f9924
--- /dev/null
+++ b/gso/workflows/router/promote_p_to_pe.py
@@ -0,0 +1,532 @@
+"""Promote a P router to a PE router."""
+
+from typing import Any
+
+from orchestrator.config.assignee import Assignee
+from orchestrator.forms import FormPage
+from orchestrator.forms.validators import Label
+from orchestrator.targets import Target
+from orchestrator.types import FormGenerator, State, UUIDstr
+from orchestrator.utils.errors import ProcessFailureError
+from orchestrator.workflow import StepList, begin, done, inputstep, step, workflow
+from orchestrator.workflows.steps import resync, store_process_subscription, unsync
+from orchestrator.workflows.utils import wrap_modify_initial_input_form
+from pydantic import ConfigDict, field_validator
+
+from gso.products.product_blocks.router import RouterRole
+from gso.products.product_types.router import Router, RouterInactive
+from gso.services import lso_client
+from gso.services.kentik_client import KentikClient, NewKentikDevice
+from gso.services.lso_client import lso_interaction
+from gso.utils.helpers import generate_inventory_for_active_routers, validate_tt_number
+
+
+def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
+    """Promote P router to PE router."""
+    subscription = Router.from_subscription(subscription_id)
+
+    class PromotePToPEForm(FormPage):
+        tt_number: str
+
+        @field_validator("tt_number")
+        def validate_tt_number(cls, tt_number: str) -> str:
+            return validate_tt_number(tt_number)
+
+    user_input = yield PromotePToPEForm
+
+    return user_input.model_dump()
+
+
+@step("[DRY RUN] Evacuate the router by setting isis_overload")
+def set_isis_overload_dry(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                          process_id: UUIDstr) -> None:
+    """Perform a dry run of evacuating the router by setting isis overload."""
+    extra_vars = {
+        "dry_run": True,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Set ISIS overload",
+        "verb": "set_isis_overload",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="promote_p_to_pe.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[FOR REAL] Evacuate the router by setting isis_overload")
+def set_isis_overload_real(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                           process_id: UUIDstr) -> None:
+    """Perform a real run of evacuating the router by setting isis overload."""
+    extra_vars = {
+        "dry_run": False,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Set ISIS overload",
+        "verb": "set_isis_overload",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="promote_p_to_pe.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[DRY RUN] Deploy PE base config")
+def deploy_pe_base_config_dry(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                              process_id: UUIDstr) -> None:
+    """Perform a dry run of adding the base config to the router."""
+    extra_vars = {
+        "dry_run": True,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - deploy PE base config",
+        "verb": "deploy_pe_base_config",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="promote_p_to_pe.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[FOR REAL] Deploy PE base config")
+def deploy_pe_base_config_real(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                               process_id: UUIDstr) -> None:
+    """Perform a real run of adding the base config to the router."""
+    extra_vars = {
+        "dry_run": False,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - deploy PE base config",
+        "verb": "deploy_pe_base_config",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="promote_p_to_pe.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@inputstep("Prompt EARL insertion", assignee=Assignee.SYSTEM)
+def prompt_insert_in_earl(subscription: dict[str, Any]) -> FormGenerator:
+    """Wait for confirmation from an operator that the router has been inserted in EARL."""
+
+    class EARLPrompt(FormPage):
+        model_config = ConfigDict(title="Update RADIUS clients")
+
+        info_label: Label = (
+            f"Please add the router {subscription["router"]["router_fqdn"]} to EARL."
+        )
+
+    yield EARLPrompt
+
+    return {}
+
+
+@step("Create Kentik device")
+def create_kentik_device(subscription: RouterInactive) -> State:
+    """Create a new device in Kentik."""
+    if not (
+            subscription.router.router_site
+            and subscription.router.router_site.site_name
+            and subscription.router.router_site.site_tier
+            and subscription.router.router_fqdn
+    ):
+        msg = "Router object is missing required properties."
+        raise ProcessFailureError(msg)
+
+    kentik_client = KentikClient()
+    kentik_site = kentik_client.get_site_by_name(subscription.router.router_site.site_name)
+
+    if not kentik_site:
+        msg = f"Site could not be found in Kentik: {subscription.router.router_site.site_name}"
+        raise ProcessFailureError(msg)
+
+    site_tier = subscription.router.router_site.site_tier
+    new_device = NewKentikDevice(
+        device_name=subscription.router.router_fqdn,
+        device_description=str(subscription.subscription_id),
+        sending_ips=[str(subscription.router.router_lo_ipv4_address)],
+        site_tier=site_tier,
+        site_id=kentik_site["id"],
+        device_snmp_ip=str(subscription.router.router_lo_ipv4_address),
+        device_bgp_flowspec=False,
+        device_bgp_neighbor_ip=str(subscription.router.router_lo_ipv4_address),
+        device_bgp_neighbor_ip6=str(subscription.router.router_lo_ipv6_address),
+    )
+    kentik_device = kentik_client.create_device(new_device)
+
+    if "error" in kentik_device or "kentik_error" in kentik_device:
+        raise ProcessFailureError(str(kentik_device))
+
+    kentik_device.pop("custom_column_data", None)
+    return {"kentik_device": kentik_device}
+
+
+@step("[DRY RUN] Remove PE from P")
+def remove_pe_from_p_dry(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                         process_id: UUIDstr) -> None:
+    """Perform a dry run of removing the PE routers from the P router."""
+    extra_vars = {
+        "dry_run": True,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+                          f"Depopulate P-only neighbour list from the promoted router",
+        "verb": "remove_pe_from_p",
+        "pe_router_list": generate_inventory_for_active_routers(RouterRole.PE)
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="update_ibgp_mesh.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[FOR REAL] Remove PE from P")
+def remove_pe_from_p_real(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                          process_id: UUIDstr) -> None:
+    """Remove the PE routers from the P router."""
+    extra_vars = {
+        "dry_run": False,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+                          f"Depopulate P-only neighbour list from the promoted router",
+        "verb": "remove_pe_from_p",
+        "pe_router_list": generate_inventory_for_active_routers(RouterRole.PE)
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="update_ibgp_mesh.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[DRY RUN] Add PE mesh to PE")
+def add_pe_mesh_to_pe_dry(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                          process_id: UUIDstr) -> None:
+    """Perform a dry run of adding list of PE routers into iGEANT/iGEANT6 of promoted router."""
+    extra_vars = {
+        "dry_run": True,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+                          f"Add list of PE routers into iGEANT/iGEANT6 of promoted router",
+        "verb": "add_pe_mesh_to_pe",
+        "pe_router_list": generate_inventory_for_active_routers(RouterRole.PE)
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="update_ibgp_mesh.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[FOR REAL] Add PE mesh to PE")
+def add_pe_mesh_to_pe_real(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                           process_id: UUIDstr) -> None:
+    """Perform a real run of adding list of PE routers into iGEANT/iGEANT6 of promoted router."""
+    extra_vars = {
+        "dry_run": False,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+                          f"Add list of PE routers into iGEANT/iGEANT6 of promoted router",
+        "verb": "add_pe_mesh_to_pe",
+        "pe_router_list": generate_inventory_for_active_routers(RouterRole.PE)
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="update_ibgp_mesh.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[DRY RUN] Add PE to PE mesh")
+def add_pe_to_pe_mesh_dry(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                          process_id: UUIDstr) -> None:
+    """Perform a dry run of adding the promoted router to all PE routers in iGEANT/iGEANT6."""
+    extra_vars = {
+        "dry_run": True,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+                          f"Add promoted router to all PE routers in iGEANT/iGEANT.",
+        "verb": "add_pe_to_pe_mesh",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="update_ibgp_mesh.yaml",
+        callback_route=callback_route,
+        inventory=generate_inventory_for_active_routers(RouterRole.PE),
+        extra_vars=extra_vars,
+    )
+
+
+@step("[FOR REAL] Add PE to PE mesh")
+def add_pe_to_pe_mesh_real(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                           process_id: UUIDstr) -> None:
+    """Perform a real run of adding the promoted router to all PE routers in iGEANT/iGEANT6."""
+    extra_vars = {
+        "dry_run": False,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+                          f"Add promoted router to all PE routers in iGEANT/iGEANT.",
+        "verb": "add_pe_to_pe_mesh",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="update_ibgp_mesh.yaml",
+        callback_route=callback_route,
+        inventory=generate_inventory_for_active_routers(RouterRole.PE),
+        extra_vars=extra_vars,
+    )
+
+
+@step("[DRY RUN] Check iBGP session")
+def check_pe_ibgp_dry(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                      process_id: UUIDstr) -> None:
+    """Perform a dry run of checking the iBGP session."""
+    extra_vars = {
+        "dry_run": True,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Check iBGP session",
+        "verb": "check_pe_ibgp",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="check_ibgp.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[FOR REAL] Check iBGP session")
+def check_pe_ibgp_real(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                       process_id: UUIDstr) -> None:
+    """Perform a real run of checking the iBGP session."""
+    extra_vars = {
+        "dry_run": False,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Check iBGP session",
+        "verb": "check_pe_ibgp",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="check_ibgp.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[DRY RUN] Deploy routing instances")
+def deploy_routing_instances_dry(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                                 process_id: UUIDstr) -> None:
+    """Perform a dry run of deploying routing instances."""
+    extra_vars = {
+        "dry_run": True,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deploy PE base config",
+        "verb": "deploy_routing_instances",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="promote_p_to_pe.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[FOR REAL] Deploy routing instances")
+def deploy_routing_instances_real(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                                  process_id: UUIDstr) -> None:
+    """Perform a real run of deploying routing instances."""
+    extra_vars = {
+        "dry_run": False,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Deploy PE base config",
+        "verb": "deploy_routing_instances",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="promote_p_to_pe.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[DRY RUN] Check L3 services")
+def check_l3_services_dry(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                          process_id: UUIDstr) -> None:
+    """Perform a dry run of checking L3 services."""
+    extra_vars = {
+        "dry_run": True,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Check L3 services",
+        "verb": "check_base_ris",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="check_l3_services.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[FOR REAL] Check L3 services")
+def check_l3_services_real(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                           process_id: UUIDstr) -> None:
+    """Perform a real run of checking L3 services."""
+    extra_vars = {
+        "dry_run": False,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Check L3 services",
+        "verb": "check_base_ris",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="check_l3_services.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[DRY RUN] Remove isis overload")
+def remove_isis_overload_dry(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                             process_id: UUIDstr) -> None:
+    """Perform a dry run of removing isis overload."""
+    extra_vars = {
+        "dry_run": True,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Remove ISIS overload",
+        "verb": "remove_isis_overload",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="promote_p_to_pe.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[FOR REAL] Remove isis overload")
+def remove_isis_overload_real(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                              process_id: UUIDstr) -> None:
+    """Perform a real run of removing isis overload."""
+    extra_vars = {
+        "dry_run": False,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Remove ISIS overload",
+        "verb": "remove_isis_overload",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="promote_p_to_pe.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("Set router role to PE (Update subscription model)")
+def update_subscription_model(subscription: Router) -> State:
+    """Update the subscription model to set router role to PE."""
+    subscription.router.router_role = RouterRole.PE
+
+    return {"subscription": subscription}
+
+
+@step("[DRY RUN] Delete default routes")
+def delete_default_routes_dry(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                              process_id: UUIDstr) -> None:
+    """Perform a dry run of deleting the default routes."""
+    extra_vars = {
+        "dry_run": True,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+                          f"Delete static default routes (part of P base-config)",
+        "verb": "delete_default_routes",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="promote_p_to_pe.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@step("[FOR REAL] Delete default routes")
+def delete_default_routes_real(subscription: dict[str, Any], callback_route: str, tt_number: str,
+                               process_id: UUIDstr) -> None:
+    """Perform a real run of deleting the default routes."""
+    extra_vars = {
+        "dry_run": False,
+        "subscription": subscription,
+        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - "
+                          f"Delete static default routes (part of P base-config)",
+        "verb": "delete_default_routes",
+    }
+
+    lso_client.execute_playbook(
+        playbook_name="promote_p_to_pe.yaml",
+        callback_route=callback_route,
+        inventory=subscription["router"]["router_fqdn"],
+        extra_vars=extra_vars,
+    )
+
+
+@workflow(
+    "Promote P router to PE router",
+    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
+    target=Target.MODIFY,
+)
+def promote_p_to_pe() -> StepList:
+    """Promote a P router to a PE router."""
+    return (
+            begin
+            >> store_process_subscription(Target.MODIFY)
+            >> unsync
+            >> lso_interaction(set_isis_overload_dry)
+            >> lso_interaction(set_isis_overload_real)
+            >> lso_interaction(deploy_pe_base_config_dry)
+            >> lso_interaction(deploy_pe_base_config_real)
+            >> prompt_insert_in_earl
+            >> create_kentik_device
+            >> lso_interaction(remove_pe_from_p_dry)
+            >> lso_interaction(remove_pe_from_p_real)
+            >> lso_interaction(add_pe_mesh_to_pe_dry)
+            >> lso_interaction(add_pe_mesh_to_pe_real)
+            >> lso_interaction(add_pe_to_pe_mesh_dry)
+            >> lso_interaction(add_pe_to_pe_mesh_real)
+            >> lso_interaction(check_pe_ibgp_dry)
+            >> lso_interaction(check_pe_ibgp_real)
+            >> lso_interaction(deploy_routing_instances_dry)
+            >> lso_interaction(deploy_routing_instances_real)
+            >> lso_interaction(check_l3_services_dry)
+            >> lso_interaction(check_l3_services_real)
+            >> lso_interaction(remove_isis_overload_dry)
+            >> lso_interaction(remove_isis_overload_real)
+            >> update_subscription_model
+            >> lso_interaction(delete_default_routes_dry)
+            >> lso_interaction(delete_default_routes_real)
+            >> resync
+            >> done
+    )