diff --git a/src/aerie_cli/commands/expansion.py b/src/aerie_cli/commands/expansion.py
index 998ee619..8ad2303d 100644
--- a/src/aerie_cli/commands/expansion.py
+++ b/src/aerie_cli/commands/expansion.py
@@ -4,12 +4,14 @@
 from pathlib import Path
 import fnmatch
 
+import arrow
+
 from rich.console import Console
 from rich.table import Table
 
 from aerie_cli.commands.command_context import CommandContext
 from aerie_cli.utils.prompts import select_from_list
-from aerie_cli.schemas.client import ExpansionRun
+from aerie_cli.schemas.client import ExpansionRun, ExpansionDeployConfiguration
 
 app = typer.Typer()
 sequences_app = typer.Typer()
@@ -19,6 +21,108 @@
 app.add_typer(runs_app, name='runs', help='Commands for expansion runs')
 app.add_typer(sets_app, name='sets', help='Commands for expansion sets')
 
+# === Bulk Deploy Command ===
+
+@app.command('deploy')
+def bulk_deploy(
+    model_id: int = typer.Option(
+        ..., '--model-id', '-m', prompt='Mission Model ID',
+        help='Mission Model ID'
+    ),
+    command_dictionary_id: int = typer.Option(
+        ..., '--command-dict-id', '-d', prompt='Command Dictionary ID',
+        help='Command Dictionary ID'
+    ),
+    config_file: str = typer.Option(
+        ..., "--config-file", "-c", prompt="Configuration file",
+        help="Deploy configuration JSON file"
+    ),
+    rules_path: Path = typer.Option(
+        Path.cwd(), help="Path to folder containing expansion rule files"
+    ),
+    time_tag: bool = typer.Option(False, help="Append time tags to create unique expansion rule/set names")
+):
+    """
+    Bulk deploy command expansion rules and sets to an Aerie instance according to a JSON configuration file.
+
+    The configuration file contains a list of rules and a list of sets:
+
+    ```
+    {
+        "rules": [...],
+        "sets": [...]
+    }
+    ```
+
+    Each rule must provide a unique rule name, the activity type name, and the name of the file with expansion logic:
+
+    ```
+    {
+        "name": "Expansion Rule Name",
+        "activity_type": "Activity Type Name",
+        "file_name": "my_file.ts"
+    }
+    ```
+
+    Each set must provide a unique set name and a list of rule names to add:
+
+    ```
+    {
+        "name": "Expansion Set Name",
+        "rules": ["Expansion Rule Name", ...]
+    }
+    ```
+    """
+
+    client = CommandContext.get_client()
+
+    with open(Path(config_file), "r") as fid:
+        configuration: ExpansionDeployConfiguration = ExpansionDeployConfiguration.from_dict(json.load(fid))
+
+    name_suffix = arrow.utcnow().format("_YYYY-MM-DDTHH-mm-ss") if time_tag else ""
+
+    # Loop and upload all expansion rules
+    uploaded_rules = {}
+    for rule in configuration.rules:
+        try:
+            with open(rules_path.joinpath(rule.file_name), "r") as fid:
+                expansion_logic = fid.read()
+
+            rule_id = client.create_expansion_rule(
+                expansion_logic=expansion_logic,
+                activity_name=rule.activity_type,
+                model_id=model_id,
+                command_dictionary_id=command_dictionary_id,
+                name=rule.name + name_suffix
+            )
+            typer.echo(f"Created expansion rule {rule.name + name_suffix}: {rule_id}")
+            uploaded_rules[rule.name] = rule_id
+        except:
+            typer.echo(f"Failed to create expansion rule {rule.name}")
+
+    for set in configuration.sets:
+        try:
+            rule_ids = []
+            for rule_name in set.rules:
+                if rule_name in uploaded_rules.keys():
+                    rule_ids.append(uploaded_rules[rule_name])
+                else:
+                    typer.echo(f"No uploaded rule {rule_name} for set {set.name}")
+            
+            assert len(rule_ids)
+
+            set_id = client.create_expansion_set(
+                command_dictionary_id=command_dictionary_id,
+                model_id=model_id,
+                expansion_ids=rule_ids,
+                name=set.name + name_suffix
+            )
+
+            typer.echo(f"Created expansion set {set.name + name_suffix}: {set_id}")
+        except:
+            typer.echo(f"Failed to create expansion set {set.name}")
+
+
 # === Commands for expansion runs ===
 
 
diff --git a/src/aerie_cli/schemas/client.py b/src/aerie_cli/schemas/client.py
index 9b84bec8..c9497f1c 100644
--- a/src/aerie_cli/schemas/client.py
+++ b/src/aerie_cli/schemas/client.py
@@ -392,3 +392,28 @@ class ExpansionRule(ClientSerialize):
 class ResourceType(ClientSerialize):
     name: str
     schema: Dict
+
+
+@define
+class ExpansionDeployRule(ClientSerialize):
+    name: str
+    activity_type: str
+    file_name: str
+
+
+@define
+class ExpansionDeploySet(ClientSerialize):
+    name: str
+    rules: List[str]
+
+
+@define
+class ExpansionDeployConfiguration(ClientSerialize):
+    rules: List[ExpansionDeployRule] = field(
+        converter=converters.optional(
+            lambda x: [ExpansionDeployRule.from_dict(d) if isinstance(d, dict) else d for d in x])
+    )
+    sets: List[ExpansionDeploySet] = field(
+        converter=converters.optional(
+            lambda x: [ExpansionDeploySet.from_dict(d) if isinstance(d, dict) else d for d in x])
+    )
diff --git a/tests/integration_tests/files/expansion/BakeBananaBread_exp.ts b/tests/integration_tests/files/expansion/BakeBananaBread_exp.ts
new file mode 100644
index 00000000..5efc4564
--- /dev/null
+++ b/tests/integration_tests/files/expansion/BakeBananaBread_exp.ts
@@ -0,0 +1,6 @@
+export default function MyExpansion(props: {
+    activityInstance: ActivityType
+}): ExpansionReturn {
+    const { activityInstance } = props
+    return []
+}
\ No newline at end of file
diff --git a/tests/integration_tests/files/expansion/BiteBanana_exp.ts b/tests/integration_tests/files/expansion/BiteBanana_exp.ts
new file mode 100644
index 00000000..5efc4564
--- /dev/null
+++ b/tests/integration_tests/files/expansion/BiteBanana_exp.ts
@@ -0,0 +1,6 @@
+export default function MyExpansion(props: {
+    activityInstance: ActivityType
+}): ExpansionReturn {
+    const { activityInstance } = props
+    return []
+}
\ No newline at end of file
diff --git a/tests/integration_tests/files/expansion/expansion_deploy_config.json b/tests/integration_tests/files/expansion/expansion_deploy_config.json
new file mode 100644
index 00000000..78e4f6d0
--- /dev/null
+++ b/tests/integration_tests/files/expansion/expansion_deploy_config.json
@@ -0,0 +1,28 @@
+{
+    "rules": [
+        {
+            "name": "integration_test_BakeBananaBread",
+            "activity_type": "BakeBananaBread",
+            "file_name": "BakeBananaBread_exp.ts"
+        },
+        {
+            "name": "integration_test_BiteBanana",
+            "activity_type": "BiteBanana",
+            "file_name": "BiteBanana_exp.ts"
+        },
+        {
+            "name": "integration_test_bad",
+            "activity_type": "Fake",
+            "file_name": "this path no exist"
+        }
+    ],
+    "sets": [
+        {
+            "name": "integration_test_set",
+            "rules": [
+                "integration_test_BakeBananaBread",
+                "integration_test_BiteBanana"
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/tests/integration_tests/test_expansion.py b/tests/integration_tests/test_expansion.py
index c004fdb8..0e882e37 100644
--- a/tests/integration_tests/test_expansion.py
+++ b/tests/integration_tests/test_expansion.py
@@ -37,6 +37,8 @@
 # Expansion Variables
 expansion_set_id = -1
 expansion_sequence_id = 1
+EXPANSION_FILES_PATH = os.path.join(FILES_PATH, "expansion")
+EXPANSION_DEPLOY_CONFIG_PATH = os.path.join(EXPANSION_FILES_PATH, "expansion_deploy_config.json")
 
 @pytest.fixture(scope="module", autouse=True)
 def set_up_environment(request):
@@ -127,6 +129,33 @@ def test_expansion_sequence_delete():
 # Uses model, command dictionary, and activity types
 #######################
 
+
+def test_expansion_deploy():
+    result = runner.invoke(
+        app,
+        [
+            "expansion",
+            "deploy",
+            "-m",
+            str(model_id),
+            "-d",
+            str(command_dictionary_id),
+            "-c",
+            EXPANSION_DEPLOY_CONFIG_PATH,
+            "--rules-path",
+            EXPANSION_FILES_PATH,
+            "--time-tag"
+        ],
+        catch_exceptions=False
+    )
+    assert result.exit_code == 0, \
+        f"{result.stdout}"\
+        f"{result.stderr}"
+    assert "Created expansion rule integration_test_BakeBananaBread" in result.stdout
+    assert "Created expansion rule integration_test_BiteBanana" in result.stdout
+    assert "Failed to create expansion rule integration_test_bad" in result.stdout
+    assert "Created expansion set integration_test_set" in result.stdout
+
 def test_expansion_set_create():
     client.create_expansion_rule(
         expansion_logic="""