diff --git a/src/InSillyCloWeb/assemblies/admin.py b/src/InSillyCloWeb/assemblies/admin.py
index 35d97a12c4ece73a8b39ab7cf78790e665bdd22f..00babc05bdf75ee4b6a5514a31ce817c8e08277d 100644
--- a/src/InSillyCloWeb/assemblies/admin.py
+++ b/src/InSillyCloWeb/assemblies/admin.py
@@ -49,3 +49,40 @@ class UserAdmin(auth_admin.UserAdmin):
         'is_staff',
         'is_superuser',
     )
+
+
+@admin.register(models.SimulatorJob)
+class JobAdmin(
+    ViewOnSiteModelAdmin,
+):
+    ordering = ("created_at",)
+    date_hierarchy = 'created_at'
+    list_display = (
+        'uuid',
+        'status',
+        'created_at',
+        'updated_at',
+    )
+    search_fields = (
+        'uuid',
+        'name',
+    )
+    actions = [
+        "re_run_job",
+        "delete_id_media_is_missing",
+    ]
+
+    @admin.action(description="Re-run task")
+    def re_run_job(self, request, queryset):
+        for job in queryset:
+            job.run_simulator()
+
+    @admin.action(description="Delete selected jobs if run_dir is missing/empty")
+    def delete_id_media_is_missing(self, request, queryset):
+        for job in queryset:
+            if not job.job_dir.exists():
+                job.delete()
+
+    def delete_queryset(self, request, queryset):
+        for o in queryset:
+            o.delete()
diff --git a/src/InSillyCloWeb/assemblies/forms.py b/src/InSillyCloWeb/assemblies/forms.py
index bd3cd4ca24398f51f194cf43342011a90ed3bdaf..4234daeba1655fe447ad3250bf674298799fa441 100644
--- a/src/InSillyCloWeb/assemblies/forms.py
+++ b/src/InSillyCloWeb/assemblies/forms.py
@@ -1,11 +1,12 @@
+import insillyclo.main
 from crispy_forms import layout
 from crispy_forms.helper import FormHelper
 from django import forms
-from django.forms import modelformset_factory
+from django.forms import modelformset_factory, modelform_factory
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext as _
 
-from assemblies.models import Assembly, InputPart
+from assemblies.models import Assembly, InputPart, SimulatorJob
 
 SEP_CHOICES = (
     (".", "Dot"),
@@ -14,6 +15,74 @@ SEP_CHOICES = (
 )
 
 
+class MultipleFileInput(forms.ClearableFileInput):
+    allow_multiple_selected = True
+
+
+class MultipleFileField(forms.FileField):
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault("widget", MultipleFileInput())
+        super().__init__(*args, **kwargs)
+
+    def clean(self, data, initial=None):
+        single_file_clean = super().clean
+        if isinstance(data, (list, tuple)):
+            result = [single_file_clean(d, initial) for d in data]
+        else:
+            result = single_file_clean(data, initial)
+        return result
+
+
+class SimulatorAssemblyTypeForm(forms.Form):
+    input_file = forms.FileField(
+        label="Load your assembly file",
+        required=True,
+        widget=forms.ClearableFileInput(attrs={'multiple': False}),
+    )
+
+
+class SimulatorDataForm(forms.Form):
+    sequence_file = forms.FileField(
+        label="Load your sequence file",
+        required=True,
+        widget=forms.ClearableFileInput(attrs={'multiple': False}),
+    )
+
+    db_ip_files = forms.FileField(
+        label="Load your db ip files",
+        required=True,
+        widget=forms.ClearableFileInput(attrs={'multiple': False}),
+    )
+
+
+class PCRModelForm(forms.ModelForm):
+    class Meta:
+        model = SimulatorJob
+        fields = ('pcr_pairs_str',)
+
+    primers_file = forms.FileField(
+        required=False,
+        label=_('Primers file'),
+        help_text=_('A CSV file where the header is \"primerId;sequence\".\nNote that the separator is \";\".'),
+    )
+
+    def save(self, commit=True):
+        instance: SimulatorJob = super().save(commit=commit)
+        if not commit or self.cleaned_data['primers_file'] is None:
+            return instance
+        output_fp = instance.job_dir / insillyclo.main.DEFAULT_CONCENTRATIONS_FILENAME
+        with open(output_fp, 'wb') as fh:
+            for chunk in self.cleaned_data['primers_file'].chunks():
+                fh.write(chunk)
+        return instance
+
+
+RestrictionEnzymeModelForm = modelform_factory(
+    model=SimulatorJob,
+    fields=('restriction_enzyme_gel',),
+)
+
+
 class AssemblyForm(forms.ModelForm):
     class Meta:
         model = Assembly
diff --git a/src/InSillyCloWeb/assemblies/migrations/0007_simulatorjob.py b/src/InSillyCloWeb/assemblies/migrations/0007_simulatorjob.py
new file mode 100644
index 0000000000000000000000000000000000000000..08e83cff557b86f7f0d119c54adaeb2efc28fadb
--- /dev/null
+++ b/src/InSillyCloWeb/assemblies/migrations/0007_simulatorjob.py
@@ -0,0 +1,44 @@
+# Generated by Django 5.1.4 on 2025-04-17 08:17
+
+import uuid
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('assemblies', '0006_alter_inputpart_options_and_more'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SimulatorJob',
+            fields=[
+                (
+                    'uuid',
+                    models.UUIDField(
+                        default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True
+                    ),
+                ),
+                ('name', models.CharField(blank=True, max_length=255, null=True)),
+                (
+                    'status',
+                    models.IntegerField(
+                        choices=[
+                            (None, '(Unknown)'),
+                            (0, 'New'),
+                            (1, 'Queued'),
+                            (5, 'Fetching'),
+                            (10, 'Running'),
+                            (15, 'Done'),
+                            (16, 'Error'),
+                            (17, 'Canceled'),
+                        ],
+                        default=0,
+                    ),
+                ),
+                ('created_at', models.DateTimeField(auto_now_add=True)),
+                ('updated_at', models.DateTimeField(auto_now=True)),
+            ],
+        ),
+    ]
diff --git a/src/InSillyCloWeb/assemblies/migrations/0008_simulatorjob_pcr_pairs_str_and_more.py b/src/InSillyCloWeb/assemblies/migrations/0008_simulatorjob_pcr_pairs_str_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..395250ba8ce2d8c821fd940da126bc9b93ddc137
--- /dev/null
+++ b/src/InSillyCloWeb/assemblies/migrations/0008_simulatorjob_pcr_pairs_str_and_more.py
@@ -0,0 +1,33 @@
+# Generated by Django 5.1.8 on 2025-04-17 14:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('assemblies', '0007_simulatorjob'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='simulatorjob',
+            name='pcr_pairs_str',
+            field=models.TextField(
+                blank=True,
+                default='',
+                help_text='In a CSV-like format where you have one pair per line, the forward primer first, then the reverse primer.',
+                verbose_name='PCR Pairs to use',
+            ),
+        ),
+        migrations.AddField(
+            model_name='simulatorjob',
+            name='restriction_enzyme_gel',
+            field=models.TextField(
+                blank=True,
+                default='',
+                help_text='Enzyme used to produce digestion gel',
+                verbose_name='Restriction enzyme',
+            ),
+        ),
+    ]
diff --git a/src/InSillyCloWeb/assemblies/models.py b/src/InSillyCloWeb/assemblies/models.py
index d2dd6116506cb0d0dce7dceeb328b25080e56001..86de26538dc163613e27e3f1378a80a075c241ec 100644
--- a/src/InSillyCloWeb/assemblies/models.py
+++ b/src/InSillyCloWeb/assemblies/models.py
@@ -4,6 +4,11 @@ import re
 from io import BytesIO
 from tempfile import NamedTemporaryFile
 from typing import List, Tuple
+import uuid
+import os
+import zipfile
+from pathlib import Path
+import shutil
 
 from django.contrib.auth import models as auth_models
 from django.contrib.sessions.models import Session
@@ -11,10 +16,15 @@ from django.db import models
 from django.db.models.functions import Upper
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
+from django.conf import settings
+from .insillyclo_impl import InSillyCloDjangoMessageObserver
+
 
 import insillyclo.data_source
 import insillyclo.models
 import insillyclo.template_generator
+import insillyclo
+import insillyclo.simulator
 
 
 class UserManager(auth_models.UserManager):
@@ -231,3 +241,146 @@ class Assembly(models.Model):
 
     def get_absolute_url(self):
         return reverse("assemblies:assembly-detail", args=[self.id])
+
+
+class JobStatus(models.IntegerChoices):
+    NEW = 0, _("New")
+    QUEUED = 1, _("Queued")
+    FETCHING = 5, _("Fetching")
+    RUNNING = 10, _("Running")
+    DONE = 15, _("Done")
+    ERROR = 16, _("Error")
+    CANCELED = 17, _("Canceled")
+
+    __empty__ = "(Unknown)"
+
+
+class SimulatorJob(models.Model):
+    #########################################################################
+    # Job attributs and parameters
+    #########################################################################
+    uuid = models.UUIDField(
+        primary_key=True,
+        default=uuid.uuid4,
+        editable=False,
+        unique=True,
+    )
+    name = models.CharField(
+        max_length=255,
+        null=True,
+        blank=True,
+    )
+    status = models.IntegerField(choices=JobStatus.choices, default=JobStatus.NEW)
+    created_at = models.DateTimeField(auto_now_add=True)
+    updated_at = models.DateTimeField(auto_now=True)
+
+    #########################################################################
+    # InSillyClo simulator attributs and parameters
+    #########################################################################
+    pcr_pairs_str = models.TextField(
+        default='',
+        blank=True,
+        verbose_name="PCR Pairs to use",
+        help_text="In a CSV-like format where you have one pair per line, "
+        "the forward primer first, then the reverse primer.",
+    )
+    restriction_enzyme_gel = models.TextField(
+        default='',
+        blank=True,
+        verbose_name="Restriction enzyme",
+        help_text="Enzyme used to produce digestion gel",
+    )
+
+    #########################################################################
+    # Function and methods
+    #########################################################################
+    def save(self, *args, **kwargs):
+        self.job_dir.mkdir(parents=True, exist_ok=True)
+
+        super().save(*args, **kwargs)
+
+    def add_design_to_dir(self, design_file):
+        file_name = os.path.join(self.job_dir, 'design_file.xlsx')
+
+        with open(file_name, 'wb+') as f:
+            for chunk in design_file.chunks():
+                f.write(chunk)
+
+    def add_gb_to_dir(self, gb_file):
+        if not os.path.isdir(self.genbank_dir):
+            os.mkdir(self.genbank_dir)
+
+        with zipfile.ZipFile(gb_file, 'r') as zip:
+            for member in zip.namelist():
+                if Path(member).suffix == '.gb' and not Path(member).stem.startswith('.'):
+                    source = zip.open(member)
+                    new_filename = os.path.join(self.genbank_dir, Path(member).stem + '.gb')
+                    with open(new_filename, 'wb') as dest:
+                        shutil.copyfileobj(source, dest)
+
+    def add_db_ip_to_dir(self, db_ip_file):
+        if not os.path.isdir(self.dbip_dir):
+            os.mkdir(self.dbip_dir)
+
+        with zipfile.ZipFile(db_ip_file, 'r') as zip:
+            for member in zip.namelist():
+                if not Path(member).stem.startswith('.'):
+                    source = zip.open(member)
+                    new_filename = os.path.join(self.dbip_dir, Path(member).stem + Path(member).suffix)
+                    with open(new_filename, 'wb') as dest:
+                        shutil.copyfileobj(source, dest)
+
+    def run_insillyclo(self, request):
+        return insillyclo.simulator.compute_all(
+            observer=InSillyCloDjangoMessageObserver(
+                request=request,
+                debug=False,
+                fail_on_error=False,
+            ),
+            input_template_filled=os.path.join(self.job_dir, 'design_file.xlsx'),
+            input_parts_files=self.db_ip_files,
+            gb_plasmids=self.genbank_files,
+            output_dir=Path(self.results_dir),
+            data_source=insillyclo.data_source.DataSourceHardCodedImplementation(),
+        )
+
+    @property
+    def db_ip_files(self):
+        return [os.path.join(self.dbip_dir, file) for file in os.listdir(self.dbip_dir)]
+
+    @property
+    def genbank_files(self):
+        return [Path(os.path.join(self.genbank_dir, file)) for file in os.listdir(self.genbank_dir)]
+
+    @property
+    def job_dir(self) -> pathlib.Path:
+        return pathlib.Path(settings.MEDIA_ROOT) / 'jobs' / str(self.uuid)
+
+    @property
+    def uuid_short(self):
+        return str(self.uuid)[:8]
+
+    @property
+    def genbank_dir(self):
+        return os.path.join(self.job_dir, 'genbank')
+
+    @property
+    def dbip_dir(self):
+        return os.path.join(self.job_dir, 'db_ip')
+
+    @property
+    def results_dir(self):
+        return os.path.join(self.job_dir, 'results')
+
+    def delete(self, *args, **kwargs):
+        try:
+            shutil.rmtree(self.job_dir)
+        except OSError as e:
+            print("Error: %s - %s." % (e.filename, e.strerror))
+        return super().delete(*args, **kwargs)
+
+    def get_absolute_url(self):
+        return reverse("assemblies:simulator-detail", args=[self.uuid])
+
+    def __str__(self):
+        return f'{str(self.uuid)[:8]} - {str(self.created_at)}'
diff --git a/src/InSillyCloWeb/assemblies/templates/assemblies/index.html b/src/InSillyCloWeb/assemblies/templates/assemblies/index.html
index be1d4ae4b4090020fc827c38faa99c1fcf5b4f10..43b1a1e96bd19fe06b9b5567065f56ea302aeaa1 100644
--- a/src/InSillyCloWeb/assemblies/templates/assemblies/index.html
+++ b/src/InSillyCloWeb/assemblies/templates/assemblies/index.html
@@ -29,7 +29,7 @@
                 {%trans "Assembly simulator subtitle"%}
             </div>
             <div class="ma-body">
-                <a role="button" class="btn btn-auto">
+                <a role="button" class="btn btn-auto" href="{% url 'assemblies:simulator-create' %}">
                     {%trans "Start assembly simulator"%}
                 </a>
             </div>
diff --git a/src/InSillyCloWeb/assemblies/templates/assemblies/simulator_results.html b/src/InSillyCloWeb/assemblies/templates/assemblies/simulator_results.html
new file mode 100644
index 0000000000000000000000000000000000000000..424f371cd0f8b4d1013b1429b15ca3f761941860
--- /dev/null
+++ b/src/InSillyCloWeb/assemblies/templates/assemblies/simulator_results.html
@@ -0,0 +1,35 @@
+{% extends "base.html" %}
+{% load crispy_forms_tags %}
+{% load sstatic %}
+{% load i18n %}
+
+{% block extra_js %}
+{{block.super}}
+{% if extra_js_file %}
+<script src="{% sstatic extra_js_file %}"></script>
+{% endif%}
+{% for extra_js_file in extra_js_files %}
+<script src="{% sstatic extra_js_file %}"></script>
+{% endfor%}
+{% endblock %}
+
+{% block extra_css %}
+{{block.super}}
+{% if extra_css_file %}
+<link rel="stylesheet" href="{% sstatic extra_css_file %}"/>
+{% endif%}
+{% for extra_css_file in extra_css_files %}
+<link rel="stylesheet" href="{% sstatic extra_css_file %}"/>
+{% endfor%}
+{% endblock %}
+
+{% block container_class %}{{block.super}} {{ custom_container_class }}{% endblock %}
+
+{% block title %}Results{% endblock %}
+{% block page_title %}Results{% endblock %}
+
+{% block content %}
+<div class="formset-container col-12 col-xs-12 {%if custom_css_width %}{{custom_css_width}}{%else%}{%if not medium_width%}col-sm-10 col-sm-offset-1 offset-sm-1 col-md-8 col-md-offset-2 offset-md-2 col-lg-6 col-lg-offset-3 offset-lg-3 col-xl-4 col-xl-offset-4 offset-xl-4 col-xxl-2 col-xxl-offset-5{%endif%}{%if medium_width%}col-md-10 col-md-offset-1 offset-md-1 col-lg-8 col-lg-offset-2 offset-lg-2 col-xl-6 col-xl-offset-3 offset-xl-3 col-xxl-4 col-xxl-offset-4 col-xxxl-2 col-xxxl-offset-5{%endif%}{%endif%}">
+    {{ simulation.uuid }}
+</div>
+{% endblock content %}
\ No newline at end of file
diff --git a/src/InSillyCloWeb/assemblies/templates/assemblies/simulator_summary.html b/src/InSillyCloWeb/assemblies/templates/assemblies/simulator_summary.html
new file mode 100644
index 0000000000000000000000000000000000000000..3ac70fb832f72832e498ac89271b6b47d9d3d269
--- /dev/null
+++ b/src/InSillyCloWeb/assemblies/templates/assemblies/simulator_summary.html
@@ -0,0 +1,108 @@
+{% extends "base.html" %}
+{% load crispy_forms_tags %}
+{% load sstatic %}
+{% load i18n %}
+
+{% block extra_js %}
+{{block.super}}
+{% if extra_js_file %}
+<script src="{% sstatic extra_js_file %}"></script>
+{% endif%}
+{% for extra_js_file in extra_js_files %}
+<script src="{% sstatic extra_js_file %}"></script>
+{% endfor%}
+{% endblock %}
+
+{% block extra_css %}
+{{block.super}}
+{% if extra_css_file %}
+<link rel="stylesheet" href="{% sstatic extra_css_file %}"/>
+{% endif%}
+{% for extra_css_file in extra_css_files %}
+<link rel="stylesheet" href="{% sstatic extra_css_file %}"/>
+{% endfor%}
+{% endblock %}
+
+{% block container_class %}{{block.super}} {{ custom_container_class }}{% endblock %}
+
+{% block title %}{{ title }}{% endblock %}
+{% block page_title %}{{ page_title|default:title }}{% endblock %}
+
+{% block content %}
+<div class="formset-container col-12 col-xs-12 {%if custom_css_width %}{{custom_css_width}}{%else%}{%if not medium_width%}col-sm-10 col-sm-offset-1 offset-sm-1 col-md-8 col-md-offset-2 offset-md-2 col-lg-6 col-lg-offset-3 offset-lg-3 col-xl-4 col-xl-offset-4 offset-xl-4 col-xxl-2 col-xxl-offset-5{%endif%}{%if medium_width%}col-md-10 col-md-offset-1 offset-md-1 col-lg-8 col-lg-offset-2 offset-lg-2 col-xl-6 col-xl-offset-3 offset-xl-3 col-xxl-4 col-xxl-offset-4 col-xxxl-2 col-xxxl-offset-5{%endif%}{%endif%}">
+    <form method="post" enctype="multipart/form-data">
+        {% csrf_token %}
+        {{ wizard.form.media }}
+        {{ wizard.management_form }}
+
+        {%if form_title %}
+        <h3 class="card-header {{form_title_class}}">
+            {{form_title}}
+        </h3>
+        {%endif%}
+        {%if step_message %}
+        <div class="card-body">
+            {{ step_message|default:"" }}
+        </div>
+        {%endif%}
+
+        <div class="row mb-4">
+            <div class="col-6">
+                <div class="card">
+                    <div class="card-header">Input plasmid</div>
+                    <table class="table table-striped">
+                    </table>
+                </div>
+            </div>
+
+            <div class="col-6">
+                <div class="card">
+                    <div class="card-header">Plasmid repository</div>
+                    <table class="table table-striped">
+                    </table>
+                </div>
+            </div>
+        </div>
+
+        <div class="card">
+            <div class="card-header">Output plasmid</div>
+            <table class="table table-striped">
+                <thead>
+                    <tr>
+                        <th scope="col">pID</th>
+                        <th scope="col">Output part type</th>
+                        <th scope="col">Input plasmid 1</th>
+                        <th scope="col">Input plasmid 2</th>
+                        <th scope="col">Input plasmid 3</th>
+                        <th scope="col">Input plasmid 4</th>
+                        <th scope="col">Input plasmid 5</th>
+                        <th scope="col">Input plasmid 6</th>
+                        <th scope="col">Input plasmid 7</th>
+                        <th scope="col">Input plasmid 8</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {% for plasmid in plasmids %}
+                    <tr>
+                        <td>{{ plasmid.plasmid_id }}</td>
+                        <td>{{ plasmid.output_type }}</td>
+                        {% for part in plasmid.parts %}
+                            <td colspan="{{ part.1.part_types|length }}">{{ part.0 }}</td>
+                        {% endfor %}
+                    </tr>
+                    {% endfor %}
+                </tbody>
+            </table>
+        </div>
+
+        <div class="text-center mt-3">
+            <a class="" role="button" href="../?reset">
+                {% trans "clear form" %}
+            </a>
+        </div>
+        <button class=" btn btn-auto {{btn_classes}}" name="submit" type="submit">
+            {{submit_text|default:"OK" }}
+        </button>
+    </form>
+</div>
+{% endblock content %}
\ No newline at end of file
diff --git a/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_detail.html b/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_detail.html
new file mode 100644
index 0000000000000000000000000000000000000000..009b9939a5ded555bf6ef8b02a4e2d9eefef71f1
--- /dev/null
+++ b/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_detail.html
@@ -0,0 +1,17 @@
+{% extends "assemblies/base.html" %}
+{% load assemblies_tags %}
+{% load i18n %}
+
+
+{% block title %}Job: {{object.uuid_short}}{% endblock %}
+{% block page_title %}Assembly simulator{% endblock %}
+{% block page_sub_title %}Assembly simulator{% endblock %}
+
+{%block content %}
+<h6 class="col-12 text-center">
+    Run on {{object.updated_at}}
+</h6>
+<h1 class="col-12 text-center">
+    TODO
+</h1>
+{%endblock content %}
\ No newline at end of file
diff --git a/src/InSillyCloWeb/assemblies/templates/assemblies/wizard_view_form_simulator_data.html b/src/InSillyCloWeb/assemblies/templates/assemblies/wizard_view_form_simulator_data.html
new file mode 100644
index 0000000000000000000000000000000000000000..8ea79ca93a5360732135f5d71f9eaf7cbdca7cd2
--- /dev/null
+++ b/src/InSillyCloWeb/assemblies/templates/assemblies/wizard_view_form_simulator_data.html
@@ -0,0 +1,116 @@
+{% extends "base.html" %}
+{% load crispy_forms_tags %}
+{% load sstatic %}
+{% load i18n %}
+
+{% block extra_js %}
+{{block.super}}
+{% if extra_js_file %}
+<script src="{% sstatic extra_js_file %}"></script>
+{% endif%}
+{% for extra_js_file in extra_js_files %}
+<script src="{% sstatic extra_js_file %}"></script>
+{% endfor%}
+{% endblock %}
+
+{% block extra_css %}
+{{block.super}}
+{% if extra_css_file %}
+<link rel="stylesheet" href="{% sstatic extra_css_file %}"/>
+{% endif%}
+{% for extra_css_file in extra_css_files %}
+<link rel="stylesheet" href="{% sstatic extra_css_file %}"/>
+{% endfor%}
+{% endblock %}
+
+{% block container_class %}{{block.super}} {{ custom_container_class }}{% endblock %}
+
+{% block title %}{{ title }}{% endblock %}
+{% block page_title %}{{ page_title|default:title }}{% endblock %}
+
+{% block content %}
+<div class="formset-container col-12 col-xs-12 {%if custom_css_width %}{{custom_css_width}}{%else%}{%if not medium_width%}col-sm-10 col-sm-offset-1 offset-sm-1 col-md-8 col-md-offset-2 offset-md-2 col-lg-6 col-lg-offset-3 offset-lg-3 col-xl-4 col-xl-offset-4 offset-xl-4 col-xxl-2 col-xxl-offset-5{%endif%}{%if medium_width%}col-md-10 col-md-offset-1 offset-md-1 col-lg-8 col-lg-offset-2 offset-lg-2 col-xl-6 col-xl-offset-3 offset-xl-3 col-xxl-4 col-xxl-offset-4 col-xxxl-2 col-xxxl-offset-5{%endif%}{%endif%}">
+    <form method="post" enctype="multipart/form-data">
+        {% csrf_token %}
+        {{ wizard.form.media }}
+        {{ wizard.management_form }}
+
+        {%if form_title %}
+        <h3 class="card-header {{form_title_class}}">
+            {{form_title}}
+        </h3>
+        {%endif%}
+        {%if step_message %}
+        <div class="card-body">
+            {{ step_message|default:"" }}
+        </div>
+        {%endif%}
+
+        <div class="card card-auto mb-4">
+            <div class="card-body">
+                Name of the assembly: {{ assembly.name }}
+                <hr>
+                Separator: {{ assembly.separator }}
+                Enzyme: {{ assembly.enzyme }}
+            </div>
+        </div>
+        
+        {% if wizard.form.forms is not None %}
+            {{ wizard.form.management_form }}
+            <div class="card card-auto mb-4">
+                <div class="card-body">
+                    {% for form in wizard.form.forms %}
+                    <div class="formset-row" data-formset-prefix="{{ wizard.form.prefix }}">
+                    {% crispy form %}
+                    {% if formset.can_delete %}
+                        {{ form.DELETE }}
+                    {% endif %}
+                    </div>
+                    {% endfor %}
+                    {% if can_add_new != False %}
+                    <div class="formset-new-item pb-4 text-center">
+                        <input type="button"
+                               class="btn btn-auto"
+                               value="{{add_new_item_to_formset_text}}"/>
+                    </div>
+                    {% endif %}
+                </div>
+            </div>
+        {% else %}
+            {% crispy wizard.form %}
+        {% endif %}
+        <div class="text-center mt-3">
+            <a class="" role="button" href="../?reset">
+                {% trans "clear form" %}
+            </a>
+        </div>
+        <div class="text-center mt-5">
+            {% if wizard.steps.prev and not hide_step_button%}
+            {% if not hide_first_step_button%}
+            <a class="btn btn-secondary" role="button" href="../{{ wizard.steps.first }}">
+                <i class="bi-chevron-double-left"></i>
+                <span class="d-none d-sm-inline">{% trans "first step" %}</span>
+            </a>
+            {% endif %}
+            <a class="btn btn-secondary" role="button" href="../{{ wizard.steps.prev }}">
+                <i class="bi-chevron-left"></i>
+                <span class="d-none d-sm-inline">{% trans "prev step" %}</span>
+            </a>
+            {% endif %}
+            <button class=" btn btn-auto {{btn_classes}}" name="submit" type="submit">
+                {{submit_text|default:"OK" }}
+            </button>
+        </div>
+    </form>
+    <div class="d-none empty_form" data-prefix="{{wizard.form.prefix}}">
+    {% block emptyform %}
+    {% if wizard.form.forms is not None %}
+    <div class="formset-row" data-formset-prefix="{{ wizard.form.prefix }}">
+        {% crispy wizard.form.empty_form  %}
+    </div>
+    {% endif %}
+    {% endblock emptyform%}
+    </div>
+</div>
+{% endblock %}
+
diff --git a/src/InSillyCloWeb/assemblies/urls.py b/src/InSillyCloWeb/assemblies/urls.py
index 4790a451d35c5cc602180f7d4185a8ab12ad9757..c35bd48462323108cd696c597131f6d945065663 100644
--- a/src/InSillyCloWeb/assemblies/urls.py
+++ b/src/InSillyCloWeb/assemblies/urls.py
@@ -7,6 +7,9 @@ designer_wizard = wizard_views.AssemblyDesignerNotProtected.as_view(
     url_name='assemblies:designer-create-step',
 )
 
+simulator_wizard = wizard_views.AssemblySimulatorNotProtected.as_view(
+    url_name='assemblies:simulator-create-step',
+)
 designer_edition_wizard = wizard_views.AssemblyDesignerEdition.as_view(
     url_name='assemblies:designer-edit-step',
 )
@@ -62,6 +65,16 @@ urlpatterns = [
     path('', views.index, name='assembly-simulator'),
     path('assembly/', views.AssemblyListView.as_view(), name='assembly-list'),
     path('assembly/<int:pk>/', views.AssemblyDetailView.as_view(), name='assembly-detail'),
+    path('assembly-simulator/create/<str:step>/', simulator_wizard, name='simulator-create-step'),
+    path('assembly-simulator/create/', simulator_wizard, name='simulator-create'),
+    path('assembly-simulator/<slug:uuid>/', views.JobSimulatorResult.as_view(), name='simulator-detail'),
+    path('assembly-simulator/<uuid:uuid>/results/', views.JobSimulatorResult.as_view(), name='simulator-results'),
+    path('assembly-simulator/<slug:uuid>/pcr-edit/', views.JobPCREdit.as_view(), name='simulator-pcr-edit'),
+    path(
+        'assembly-simulator/<slug:uuid>/enzyme-edit/',
+        views.JobRestrictionEnzymeEdit.as_view(),
+        name='simulator-enzyme-edit',
+    ),
     path('assembly/<int:pk>/download/', views.AssemblyDetailDownloadView.as_view(), name='assembly-download'),
     path('assembly/<int:pk>/delete/', views.AssemblyDeleteView.as_view(), name='assembly-delete'),
     path('fragment', views.show_fragment, name='fragment'),
diff --git a/src/InSillyCloWeb/assemblies/utils.py b/src/InSillyCloWeb/assemblies/utils.py
index 3ca48c5857efda6fa98efc11cf0237f2005d1f07..b2c1d2a023ff4270977beb7f736ba961d9507e82 100644
--- a/src/InSillyCloWeb/assemblies/utils.py
+++ b/src/InSillyCloWeb/assemblies/utils.py
@@ -1,4 +1,5 @@
 import os
+import glob
 from zipfile import ZipFile
 from Bio import SeqIO
 from Bio.Seq import Seq
@@ -56,3 +57,17 @@ def get_fragments(part, enzyme=None, site_for='GGTCTC', site_rev='GAGACC', inter
     (out_part, in_part) = get_in_out_part(reorder_seq(seq, site_for), site_rev)
 
     return get_sens_antisens(in_part, out_part, inter_s)
+
+
+def get_files(path, extension, recursive=False):
+    """
+    A generator of filepaths for each file into path with the target extension.
+    If recursive, it will loop over subfolders as well.
+    """
+    if not recursive:
+        for file_path in glob.iglob(path + "/*." + extension):
+            yield file_path
+    else:
+        for root, dirs, files in os.walk(path):
+            for file_path in glob.iglob(root + "/*." + extension):
+                yield file_path
diff --git a/src/InSillyCloWeb/assemblies/views.py b/src/InSillyCloWeb/assemblies/views.py
index ce4c963440fc218d098ffebab0471638b31a8523..63f83a511ab6650a7dbd0dc0c69079bccb7ae867 100644
--- a/src/InSillyCloWeb/assemblies/views.py
+++ b/src/InSillyCloWeb/assemblies/views.py
@@ -1,13 +1,15 @@
 import traceback
+from abc import ABC, abstractmethod
 from io import BytesIO
 from typing import Tuple
 
+import crispy_forms.helper
 from django.contrib import messages
 from django.http import HttpResponse
-from django.shortcuts import render, redirect
+from django.shortcuts import render, redirect, get_object_or_404
 from django.urls import reverse_lazy
 from django.utils.translation import gettext_lazy as _
-from django.views.generic import ListView, DetailView, View, DeleteView
+from django.views.generic import ListView, DetailView, View, DeleteView, UpdateView
 from django.views.generic.detail import SingleObjectMixin
 
 from . import mixins
@@ -108,3 +110,64 @@ class AssemblyDeleteView(
     def form_valid(self, form):
         messages.info(self.request, _(f"Assembly {self.object} deleted"))
         return super().form_valid(form)
+
+
+class JobSimulatorResult(DetailView):
+    model = models.SimulatorJob
+    slug_field = 'uuid'
+    slug_url_kwarg = 'uuid'
+
+    template_name = "assemblies/simulator_results.html"
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context['simulation'] = self.object
+        return context
+
+
+class SimulatorJobEdit(
+    ABC,
+    UpdateView,
+):
+    model = models.SimulatorJob
+    slug_field = 'uuid'
+    slug_url_kwarg = 'uuid'
+    template_name = "assemblies/form_host.html"
+
+    def get_form(self, form_class=None):
+        form = super().get_form()
+        if not hasattr(form, 'helper'):
+            form.helper = crispy_forms.helper.FormHelper()
+            form.helper.form_tag = False
+        return form
+
+    def form_valid(self, form):
+        self.object.run_insillyclo(self.request)
+        return super().form_valid(form)
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context["title"] = self.get_title()
+        return context
+
+    @abstractmethod
+    def get_title(self):
+        pass
+
+
+class JobPCREdit(
+    SimulatorJobEdit,
+):
+    form_class = forms.PCRModelForm
+
+    def get_title(self):
+        return _("PCR primers to use for gel simulation")
+
+
+class JobRestrictionEnzymeEdit(
+    SimulatorJobEdit,
+):
+    form_class = forms.RestrictionEnzymeModelForm
+
+    def get_title(self):
+        return _("Restriction enzyme to use for gel simulation")
diff --git a/src/InSillyCloWeb/assemblies/wizard_views.py b/src/InSillyCloWeb/assemblies/wizard_views.py
index b65ec445d4cfae4e01e89339da93bb8f6ea5147e..88ef0daa38d5af1a36851c331c221726ec28ae34 100644
--- a/src/InSillyCloWeb/assemblies/wizard_views.py
+++ b/src/InSillyCloWeb/assemblies/wizard_views.py
@@ -1,15 +1,119 @@
+import insillyclo.parser
 from crispy_forms.helper import FormHelper
 from django.contrib.sessions.models import Session
 from django.db import transaction
-from django.shortcuts import redirect
+from django.shortcuts import render, redirect
+from django.conf import settings
 from django.template.loader import render_to_string
 from django.urls import reverse
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext as _
 from django.views.generic.detail import SingleObjectMixin
 from formtools.wizard import views as wizard_views
+from django.core.files.storage import FileSystemStorage
+
+import pathlib
+
+import insillyclo
+import insillyclo.simulator
+import os
 
 from assemblies import forms, models, mixins
+from .models import SimulatorJob
+from .utils import get_files
+from .insillyclo_impl import InSillyCloDjangoMessageObserver
+
+
+class AssemblySimulatorNotProtected(wizard_views.NamedUrlSessionWizardView):
+    file_storage = FileSystemStorage(location=os.path.join(settings.MEDIA_ROOT, 'assemblytype'))
+
+    form_list = [
+        ("Intro", forms.AgreeForm),
+        ("InputFile", forms.SimulatorAssemblyTypeForm),
+        ("Data", forms.SimulatorDataForm),
+        ("Summary", forms.AgreeForm),
+    ]
+
+    TEMPLATES = {
+        "Intro": 'wizard_view_form_host.html',
+        "InputFile": 'wizard_view_form_host.html',
+        "Data": 'assemblies/wizard_view_form_simulator_data.html',
+        "Summary": 'assemblies/simulator_summary.html',
+    }
+
+    is_edition = False
+    condition_dict = dict()
+
+    def get_context_data(self, form, step=None, **kwargs):
+        context = super().get_context_data(form=form, step=step, **kwargs)
+        context["submit_text"] = _("Next")
+        context["title"] = _("SimulatorDesignerWizard.title")
+        context["custom_css_width"] = "col-12 context-simulator"
+        context["form_title_class"] = "text-primary"
+        context["custom_container_class"] = "context-simulator"
+        context["hide_first_step_button"] = True
+        if step is None:
+            step = self.steps.current
+        if step == "Intro":
+            context["form_title_class"] = "h1"
+            context["title"] = ""
+            context["form_title"] = _("SimulatorDesignerWizard.Intro.form_title")
+            context["step_message"] = mark_safe(_("SimulatorDesignerWizard.Intro.step_message"))
+        elif step == "Data":
+            context['assembly'], context["plasmids"] = insillyclo.parser.parse_assembly_and_plasmid_from_template(
+                self.get_cleaned_data_for_step("InputFile")["input_file"],
+                input_part_factory=insillyclo.models.InputPartDataClassFactory(),
+                assembly_factory=insillyclo.models.AssemblyDataClassFactory(),
+                plasmid_factory=insillyclo.models.PlasmidDataClassFactory(),
+                observer=InSillyCloDjangoMessageObserver,
+            )
+        elif step == "Summary":
+            context["submit_text"] = _("Run assembly")
+            context["form_title"] = _("SimulatorDesignerWizard.Summary.form_title")
+
+            context['assembly'], context["plasmids"] = insillyclo.parser.parse_assembly_and_plasmid_from_template(
+                self.get_cleaned_data_for_step("InputFile")["input_file"],
+                input_part_factory=insillyclo.models.InputPartDataClassFactory(),
+                assembly_factory=insillyclo.models.AssemblyDataClassFactory(),
+                plasmid_factory=insillyclo.models.PlasmidDataClassFactory(),
+                observer=InSillyCloDjangoMessageObserver,
+            )
+        return context
+
+    def get_template_names(self):
+        return [self.TEMPLATES[self.steps.current]]
+
+    def get_form_initial(self, step):
+        return super().get_form_initial(step)
+
+    def get_form_kwargs(self, step=None):
+        d = super().get_form_kwargs(step=step)
+        return d
+
+    def get_form(self, *args, **kwargs):
+        form = super().get_form(*args, **kwargs)
+        if not hasattr(form, 'helper'):
+            helper = FormHelper()
+            helper.form_tag = False
+            form.helper = helper
+        return form
+
+    def done(self, form_list, form_dict=None, **kwargs):
+
+        sjob = SimulatorJob.objects.create()
+        sjob.save()
+
+        sjob.add_design_to_dir(
+            self.get_cleaned_data_for_step("InputFile")["input_file"],
+        )
+        sjob.add_gb_to_dir(
+            self.get_cleaned_data_for_step("Data")["sequence_file"],
+        )
+        sjob.add_db_ip_to_dir(
+            self.get_cleaned_data_for_step("Data")["db_ip_files"],
+        )
+
+        return redirect('assemblies:simulator-results', uuid=sjob.uuid)
 
 
 class AssemblyDesignerNotProtected(wizard_views.NamedUrlSessionWizardView):
diff --git a/src/InSillyCloWeb/insillycloweb/settings.py b/src/InSillyCloWeb/insillycloweb/settings.py
index cb7b56ba4b36153cdcc0f2c7e500e1745192e706..be6e99c3a26589c3abf7f6cdeb2c31f851c0613c 100644
--- a/src/InSillyCloWeb/insillycloweb/settings.py
+++ b/src/InSillyCloWeb/insillycloweb/settings.py
@@ -12,11 +12,9 @@ https://docs.djangoproject.com/en/5.1/ref/settings/
 
 import logging
 import os
-import os
-from pathlib import Path
 from pathlib import Path
 from socket import gethostname, gethostbyname
-from socket import gethostname, gethostbyname
+
 
 from decouple import config