diff --git a/beat/web/experiments/admin.py b/beat/web/experiments/admin.py
index 42d9d0b90a1c7a099604f4bf5be67af191d221ea..008729cf2d79b423d80166ef82a5a711e99b9b81 100644
--- a/beat/web/experiments/admin.py
+++ b/beat/web/experiments/admin.py
@@ -32,17 +32,19 @@ from django.contrib import admin
 from django.core.files.base import ContentFile
 from django.utils import six
 from django.utils.html import format_html
+from django.utils.safestring import mark_safe
 from django.core.urlresolvers import reverse
-from django.db.models import Max
+from django.db.models import Max, Count
 
 from .models import Experiment as ExperimentModel
 from .models import Block as BlockModel
 from .models import Result as ResultModel
 from .models import CachedFile as CachedFileModel
+from .models import BlockInput as BlockInputModel
 from .models import validate_experiment
 
 from ..ui.forms import CodeMirrorJSONFileField, CodeMirrorRSTFileField, \
-        NameField
+        NameField, CodeMirrorJSONCharField
 
 from ..common.texts import Messages
 
@@ -113,6 +115,27 @@ class ExperimentModelForm(forms.ModelForm):
             self.data['declaration_file'] = ContentFile(self.data['declaration_file'], name='unsaved')
 
 
+class BlockInline(admin.TabularInline):
+
+    model = BlockModel
+    extra = 0
+
+    readonly_fields = ['link', 'algorithm', 'analyzer', 'status']
+    ordering = ['id']
+    fields = readonly_fields
+
+    def link(self, obj):
+        url = reverse('admin:experiments_block_change', args=(obj.pk,))
+        return mark_safe('<a href="%s">%s</a>' % (url, obj.name))
+    link.short_description = 'name'
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+    def has_add_permission(self, request):
+            return False
+
+
 #----------------------------------------------------------
 
 
@@ -163,6 +186,10 @@ class Experiment(admin.ModelAdmin):
         'shared_with_team'
     ]
 
+    inlines = [
+            BlockInline,
+            ]
+
     fieldsets = (
         (None,
           dict(
@@ -205,9 +232,140 @@ admin.site.register(ExperimentModel, Experiment)
 #----------------------------------------------------------
 
 
+class BlockInputInline(admin.TabularInline):
+
+    model           = BlockInputModel
+    verbose_name = 'Input'
+    verbose_name_plural = 'Inputs'
+    extra           = 0
+    ordering        = ['database', 'cache']
+    readonly_fields = ['input', 'channel']
+    fields = readonly_fields
+
+    def input(self, obj):
+        if obj.database:
+            url = reverse('admin:databases_databaseset_change',
+                args=(obj.database.set.pk,))
+            text = '%s (%s)' % (obj.database, obj.database.hash)
+            what = 'Dataset Output'
+        else:
+            url = reverse('admin:experiments_cachedfile_change',
+                args=(obj.cache.pk,))
+            text = obj.cache.hash
+            what = 'Cached File'
+        return mark_safe('%s: <a href="%s">%s</a>' % (what, url, text))
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+    def has_add_permission(self, request):
+            return False
+
+
+class CachedFileInline(admin.TabularInline):
+
+    model = CachedFileModel.blocks.through
+    verbose_name = 'Output'
+    verbose_name_plural = 'Outputs'
+    extra = 0
+
+    readonly_fields = ['output']
+    fields = readonly_fields
+
+    def output(self, obj):
+        url = reverse('admin:experiments_cachedfile_change', args=(obj.cachedfile.pk,))
+        text = obj.cachedfile.hash
+        what = 'Cached File'
+        return mark_safe('%s: <a href="%s">%s</a>' % (what, url, text))
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+    def has_add_permission(self, request):
+            return False
+
+
+class BlockDependentsInline(admin.TabularInline):
+
+    model = BlockModel.dependencies.through
+    verbose_name = 'Dependent'
+    verbose_name_plural = 'Dependents'
+    fk_name = 'to_block'
+    extra = 0
+
+    readonly_fields = ['name', 'algorithm', 'analyzer', 'status']
+    ordering = ['id']
+    fields = readonly_fields
+
+    def name(self, obj):
+        url = reverse('admin:experiments_block_change', args=(obj.from_block.pk,))
+        return mark_safe('<a href="%s">%s</a>' % (url, obj.from_block.name))
+
+    def algorithm(self, obj):
+        return obj.from_block.algorithm
+
+    def analyzer(self, obj):
+        return obj.from_block.analyzer
+    analyzer.boolean = True
+
+    def status(self, obj):
+        return obj.from_block.get_status_display()
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+    def has_add_permission(self, request):
+            return False
+
+
+class BlockDependenciesInline(admin.TabularInline):
+
+    model = BlockModel.dependencies.through
+    verbose_name = 'Dependency'
+    verbose_name_plural = 'Dependencies'
+    fk_name = 'from_block'
+    extra = 0
+
+    readonly_fields = ['name', 'algorithm', 'analyzer', 'status']
+    ordering = ['id']
+    fields = readonly_fields
+
+    def name(self, obj):
+        url = reverse('admin:experiments_block_change', args=(obj.to_block.pk,))
+        return mark_safe('<a href="%s">%s</a>' % (url, obj.to_block.name))
+
+    def algorithm(self, obj):
+        return obj.to_block.algorithm
+
+    def analyzer(self, obj):
+        return obj.to_block.analyzer
+    analyzer.boolean = True
+
+    def status(self, obj):
+        return obj.to_block.get_status_display()
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+    def has_add_permission(self, request):
+            return False
+
+
+class BlockModelForm(forms.ModelForm):
+
+    command = CodeMirrorJSONCharField(
+        help_text=Messages['json'],
+        readonly=True,
+        )
+
+    class Meta:
+        model = BlockModel
+        exclude = []
+
+
 class Block(admin.ModelAdmin):
 
-    list_display        = ('id', 'experiment', 'name', 'algorithm', 'analyzer', 'status', 'environment')
+    list_display        = ('id', 'author', 'toolchain', 'xp', 'name', 'algorithm', 'analyzer', 'status', 'ins', 'outs', 'environment', 'q')
     search_fields       = ['name',
                            'experiment__author__username',
                            'experiment__toolchain__author__username',
@@ -220,6 +378,84 @@ class Block(admin.ModelAdmin):
                            ]
     list_display_links  = ('id', 'name')
 
+    inlines = [
+            BlockDependenciesInline,
+            BlockInputInline,
+            CachedFileInline,
+            BlockDependentsInline,
+            ]
+
+    exclude = ['dependencies']
+
+    def get_queryset(self, request):
+        qs = super(Block, self).get_queryset(request)
+        return qs.annotate(Count('outputs'))
+
+    def author(self, obj):
+        return obj.experiment.author
+
+    def toolchain(self, obj):
+        return obj.experiment.toolchain
+
+    def xp(self, obj):
+        return obj.experiment.name
+    xp.short_description = 'experiment'
+
+    def ins(self, obj):
+        return obj.inputs.count()
+
+    def outs(self, obj):
+        return obj.outputs__count
+    outs.admin_order_field = 'outputs__count'
+
+    def q(self, obj):
+        if obj.queue: return obj.queue.name
+        return None
+    q.short_description = 'queue'
+
+    def get_readonly_fields(self, request, obj=None):
+        return list(self.readonly_fields) + \
+            [field.name for field in obj._meta.fields if field.name != 'command']
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+    def has_add_permission(self, request):
+            return False
+
+    form = BlockModelForm
+
+    fieldsets = (
+        (None,
+          dict(
+            fields=('id', 'name', 'experiment'),
+            ),
+          ),
+        ('Status and dates',
+          dict(
+            fields=('creation_date', 'start_date', 'end_date', 'runnable_date', 'status'),
+            ),
+          ),
+        ('Code',
+          dict(
+            classes=('collapse',),
+            fields=('algorithm', 'analyzer',),
+            ),
+          ),
+        ('Backend',
+          dict(
+            classes=('collapse',),
+            fields=('environment', 'queue', 'required_slots', 'channel'),
+            ),
+          ),
+        ('Command',
+          dict(
+            classes=('collapse',),
+            fields=('command',),
+            ),
+          ),
+        )
+
 admin.site.register(BlockModel, Block)
 
 
@@ -228,19 +464,25 @@ admin.site.register(BlockModel, Block)
 
 class Result(admin.ModelAdmin):
 
-    list_display        = ('id', 'block', 'name', 'type', 'primary', 'data_value')
+    list_display        = ('id', 'cache', 'name', 'type', 'primary', 'data_value')
 
     search_fields       = [
             'name',
-            'type',
-            'block__name',
-            'block__experiment__name',
-            'block__experiment__author__username',
-            'block__experiment__toolchain__name',
+            'cache__hash',
             ]
 
     list_display_links  = ('id', 'name')
 
+    def get_readonly_fields(self, request, obj=None):
+        return list(self.readonly_fields) + \
+               [field.name for field in obj._meta.fields]
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+    def has_add_permission(self, request):
+            return False
+
 admin.site.register(ResultModel, Result)
 
 
@@ -334,4 +576,16 @@ class CachedFile(admin.ModelAdmin):
           ),
         )
 
+    readonly_fields = ['blocks']
+
+    def get_readonly_fields(self, request, obj=None):
+        return list(self.readonly_fields) + \
+               [field.name for field in obj._meta.fields]
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+    def has_add_permission(self, request):
+            return False
+
 admin.site.register(CachedFileModel, CachedFile)
diff --git a/beat/web/experiments/migrations/0002_auto_20160330_0953.py b/beat/web/experiments/migrations/0002_auto_20160330_0953.py
new file mode 100644
index 0000000000000000000000000000000000000000..bdc49d4636e9e5cacfd01a21534a2a78c0678f1d
--- /dev/null
+++ b/beat/web/experiments/migrations/0002_auto_20160330_0953.py
@@ -0,0 +1,235 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models, utils
+from django.conf import settings
+
+import simplejson
+import beat.core.experiment
+from ...common import storage
+
+
+def move_result_to_cache(apps, schema_editor):
+    '''Moves the result association from the block to the related cache file'''
+
+    Result = apps.get_model("experiments", "Result")
+
+    total = Result.objects.count()
+    if total: print('')
+    for i, r in enumerate(Result.objects.all()):
+        print("Resetting result %d/%d..." % (i+1, total))
+        r.cache = r.block.hashes.first()
+        r.save()
+
+
+def reset_blocks(apps, schema_editor):
+    '''Resets block dependencies and queue relationship'''
+
+    Experiment = apps.get_model("experiments", "Experiment")
+    Block = apps.get_model("experiments", "Block")
+    BlockInput = apps.get_model("experiments", "BlockInput")
+    CachedFile = apps.get_model("experiments", "CachedFile")
+    Queue = apps.get_model("backend", "Queue")
+    Environment = apps.get_model("backend", "Environment")
+    Algorithm = apps.get_model("algorithms", "Algorithm")
+    DatabaseSetOutput = apps.get_model("databases", "DatabaseSetOutput")
+
+    total = Experiment.objects.count()
+    for i, e in enumerate(Experiment.objects.order_by('id')):
+
+        fullname = '%s/%s/%s/%d/%s' % (
+            e.author.username,
+            e.toolchain.author.username,
+            e.toolchain.name,
+            e.toolchain.version,
+            e.name,
+            )
+
+        print("Updating blocks for experiment %d/%d (%s, id=%d)..." % (i+1, total, fullname, e.id))
+
+        xp_decl = simplejson.loads(storage.get_file_content(e,
+          'declaration_file'))
+        tc_decl = simplejson.loads(storage.get_file_content(e.toolchain,
+          'declaration_file'))
+
+        xp = beat.core.experiment.Experiment(settings.PREFIX, (xp_decl,
+          tc_decl))
+
+        if xp.errors:
+            message = "The experiment `%s' isn't valid (skipping " \
+                "block update), due to the following errors:\n  * %s"
+            print message % (fullname, '\n * '.join(xp.errors))
+            continue
+
+        # Loads the experiment execution description, creating the Block's,
+        # BlockInput's and BlockOutput's as required.
+        for block_name, description in xp.setup().items():
+
+            # Checks that the Queue/Environment exists
+            job_description = description['configuration']
+
+            env = Environment.objects.filter(
+                name=job_description['environment']['name'],
+                version=job_description['environment']['version'],
+                )
+
+            if not env:
+                print("Cannot find environment `%s (%s)' - not setting" % \
+                    (job_description['environment']['name'],
+                    job_description['environment']['version']))
+                env = None
+            else:
+                env = env[0]
+
+            # Search for queue that contains a specific environment
+            # notice we don't require environment to exist in relation to
+            # the queue as it may have been removed already.
+            queue = Queue.objects.filter(name=job_description['queue'])
+            if not queue:
+                print("Cannot find queue `%s'" % job_description['queue'])
+                queue = None
+            else:
+                queue = queue[0]
+
+            parts = job_description['algorithm'].split('/')
+            algorithm = Algorithm.objects.get(
+                author__username=parts[0],
+                name=parts[1],
+                version=parts[2],
+                )
+
+            # Ties the block in
+            slots = job_description.get('nb_slots')
+
+            try:
+                b, _ = Block.objects.get_or_create(experiment=e,
+                    name=block_name, algorithm=algorithm)
+            except utils.IntegrityError as exc:
+                print("Block `%s' for experiment `%s' already exists - " \
+                    "modifying entry for migration purposes. This " \
+                    "issue is due a misconnection on the toolchain level " \
+                    "(known case: tpereira/full_isv/2)" % \
+                    (block_name, fullname))
+                b = Block.objects.get(experiment=e, name=block_name)
+
+            b.command=simplejson.dumps(job_description, indent=4)
+            b.status='N' if (e.status == 'P') else b.status
+            b.environment=env
+            b.queue=queue
+            b.algorithm = algorithm
+            b.analyzer = (algorithm.result_dataformat is not None)
+            b.required_slots=job_description['nb_slots']
+            b.channel=job_description['channel']
+            b.save()
+
+            # from this point: requires block to have an assigned id
+            b.dependencies.add(*[e.blocks.get(name=k) \
+                for k in description['dependencies']])
+
+            # reset inputs and outputs - creates if necessary only
+            for v in job_description['inputs'].values():
+                if 'database' in v: #database input
+                    db = DatabaseSetOutput.objects.get(hash=v['hash'])
+                    BlockInput.objects.get_or_create(block=b,
+                        channel=v['channel'], database=db)
+                else:
+                    cache = CachedFile.objects.get(hash=v['hash'])
+                    BlockInput.objects.get_or_create(block=b,
+                        channel=v['channel'], cache=cache)
+
+            outputs = job_description.get('outputs',
+                {'': job_description.get('result')})
+            for v in outputs.values():
+                cache, cr = CachedFile.objects.get_or_create(hash=v['hash'])
+                if cr:
+                    print("CachedFile (hash=%s) created for block `%s' of " \
+                        "experiment `%s' which is in state `%s'" % \
+                        (cache.hash, block_name, fullname,
+                          b.get_status_display()))
+                cache.blocks.add(b)
+
+        #asserts all blocks (except analysis blocks have dependents)
+        for b in e.blocks.all():
+            assert (b.analyzer and b.dependents.count() == 0) or b.dependents.count() > 0
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('backend', '0002_auto_20160330_0005'),
+        ('databases', '0002_auto_20160329_1733'),
+        ('experiments', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='result',
+            name='cache',
+            field=models.ForeignKey(related_name='results', to='experiments.CachedFile', null=True),
+        ),
+        migrations.RunPython(move_result_to_cache),
+        migrations.RemoveField(
+            model_name='result',
+            name='block',
+        ),
+        migrations.CreateModel(
+            name='BlockInput',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('channel', models.CharField(default=b'', help_text=b'Synchronization channel within the toolchain', max_length=200, blank=True)),
+                ('block', models.ForeignKey(related_name='inputs', to='experiments.Block', null=True)),
+                ('cache', models.ForeignKey(related_name='inputs', to='experiments.CachedFile', null=True)),
+                ('database', models.ForeignKey(related_name='blocks', to='databases.DatabaseSetOutput', null=True)),
+            ],
+        ),
+        migrations.AddField(
+            model_name='block',
+            name='channel',
+            field=models.CharField(default=b'', help_text=b'Synchronization channel within the toolchain', max_length=200, blank=True),
+        ),
+        migrations.AddField(
+            model_name='block',
+            name='command',
+            field=models.TextField(null=True, blank=True),
+        ),
+        migrations.AddField(
+            model_name='block',
+            name='dependencies',
+            field=models.ManyToManyField(related_name='dependents', to='experiments.Block', blank=True),
+        ),
+        migrations.AlterField(
+            model_name='block',
+            name='environment',
+            field=models.ForeignKey(related_name='blocks', on_delete=models.deletion.SET_NULL, to='backend.Environment', null=True),
+        ),
+        migrations.AddField(
+            model_name='block',
+            name='queue',
+            field=models.ForeignKey(related_name='blocks', on_delete=models.deletion.SET_NULL, to='backend.Queue', null=True),
+        ),
+        migrations.AddField(
+            model_name='block',
+            name='required_slots',
+            field=models.PositiveIntegerField(default=1),
+        ),
+        migrations.AddField(
+            model_name='block',
+            name='runnable_date',
+            field=models.DateTimeField(null=True, blank=True),
+        ),
+        migrations.AlterField(
+            model_name='block',
+            name='status',
+            field=models.CharField(default=b'N', max_length=1, choices=[(b'N', b'Not cached'), (b'P', b'Processing'), (b'C', b'Cached'), (b'F', b'Failed'), (b'S', b'Skipped'), (b'L', b'Cancelled')]),
+        ),
+        migrations.AlterUniqueTogether(
+            name='block',
+            unique_together=set([('experiment', 'name')]),
+        ),
+        migrations.AlterField(
+            model_name='cachedfile',
+            name='blocks',
+            field=models.ManyToManyField(related_name='outputs', to='experiments.Block', blank=True),
+        ),
+        migrations.RunPython(reset_blocks),
+    ]
diff --git a/beat/web/experiments/models.py b/beat/web/experiments/models.py
index e3366d373cd6d94e5d0887c6c4cd527c0660307b..53e06c7ce1ae568645fa829f79601ae851fe7cb6 100644
--- a/beat/web/experiments/models.py
+++ b/beat/web/experiments/models.py
@@ -40,7 +40,6 @@ import beat.core.experiment
 from beat.core.utils import NumpyJSONEncoder
 
 from ..algorithms.models import Algorithm
-from ..databases.models import DatabaseSet
 from ..toolchains.models import Toolchain
 
 from ..common.models import Shareable
@@ -55,6 +54,7 @@ from ..common.exceptions import ShareError
 from ..common.texts import Messages
 from ..common.storage import OverwriteStorage
 from ..backend.models import Queue, Environment
+from ..databases.models import DatabaseSet,  DatabaseSetOutput
 from ..import __version__
 
 
@@ -217,8 +217,10 @@ class Experiment(Shareable):
 
     #_____ Fields __________
 
-    author            = models.ForeignKey(User, related_name='experiments')
-    toolchain         = models.ForeignKey(Toolchain, related_name='experiments')
+    author            = models.ForeignKey(User, related_name='experiments',
+        on_delete=models.CASCADE)
+    toolchain         = models.ForeignKey(Toolchain,
+        related_name='experiments', on_delete=models.CASCADE)
     name              = models.CharField(max_length=200)
     short_description = models.CharField(max_length=100, default='', blank=True, help_text=Messages['short_description'])
     status            = models.CharField(max_length=1, choices=STATUS, default=PENDING)
@@ -244,8 +246,8 @@ class Experiment(Shareable):
 
 
     # read-only parameters that are updated at every save(), if required
-    hash                  = models.CharField(max_length=64)
-    referenced_datasets   = models.ManyToManyField(DatabaseSet, related_name='experiments', blank=True)
+    hash = models.CharField(max_length=64)
+    referenced_datasets = models.ManyToManyField(DatabaseSet, related_name='experiments', blank=True)
     referenced_algorithms = models.ManyToManyField(Algorithm, related_name='experiments', blank=True)
 
     objects = ExperimentManager()
@@ -409,6 +411,9 @@ class Experiment(Shareable):
             storage.rename_file(self, 'description_file', self.description_filename())
 
         if content_modified:
+            # Creates experiment blocks and setup dependencies
+            self.update_blocks()
+
             # Link the experiment to the datasets
             self.referenced_datasets.clear()
             for dataset_declaration in xp.datasets.values():
@@ -485,6 +490,88 @@ class Experiment(Shareable):
         super(Experiment, self).share(users=users, teams=teams)
 
 
+    def update_blocks(self):
+        """Updates internal block representation of an experiment"""
+
+        corexp = self.core()
+
+        # Loads the experiment execution description, creating the Block's,
+        # BlockInput's and BlockOutput's as required.
+        for block_name, description in corexp.setup().items():
+
+            # Checks that the Queue/Environment exists
+            job_description = description['configuration']
+
+            env = Environment.objects.filter(
+                name=job_description['environment']['name'],
+                version=job_description['environment']['version'],
+                )
+
+            if not env:
+                logger.warn("Cannot find environment `%s (%s)' - not setting",
+                    job_description['environment']['name'],
+                    job_description['environment']['version'])
+                env = None
+            else:
+                env = env[0]
+
+            # Search for queue that contains a specific environment
+            if env:
+              queue = Queue.objects.filter(name=job_description['queue'],
+                environments__in=[env])
+            else:
+              queue = Queue.objects.filter(name=queue)
+            if not queue:
+                env_name = env.fullname() if env else 'NULL'
+                logger.warn("Cannot find queue `%s' which contains " \
+                    "environment `%s' - not setting",
+                    job_description['queue'], env_name)
+                queue = None
+            else:
+                queue = queue[0]
+
+            parts = job_description['algorithm'].split('/')
+            algorithm = Algorithm.objects.get(
+                author__username=parts[0],
+                name=parts[1],
+                version=parts[2],
+                )
+
+            # Ties the block in
+            slots = job_description.get('nb_slots')
+            b, _ = Block.objects.get_or_create(experiment=self,
+                name=block_name, algorithm=algorithm)
+            b.command=simplejson.dumps(job_description, indent=4)
+            b.status=Block.NOT_CACHED if (self.status == Experiment.PENDING) else b.status
+            b.analyzer=algorithm.analysis()
+            b.environment=env
+            b.queue=queue
+            b.required_slots=job_description['nb_slots']
+            b.channel=job_description['channel']
+            b.save()
+
+            # from this point: requires block to have an assigned id
+            b.dependencies.add(*[self.blocks.get(name=k) \
+                for k in description['dependencies']])
+
+            # reset inputs and outputs - creates if necessary only
+            for v in job_description['inputs'].values():
+                if 'database' in v: #database input
+                    db = DatabaseSetOutput.objects.get(hash=v['hash'])
+                    BlockInput.objects.get_or_create(block=b,
+                        channel=v['channel'], database=db)
+                else:
+                    cache = CachedFile.objects.get(hash=v['hash'])
+                    BlockInput.objects.get_or_create(block=b,
+                        channel=v['channel'], cache=cache)
+
+            outputs = job_description.get('outputs',
+                {'': job_description.get('result')})
+            for v in outputs.values():
+                cache, _ = CachedFile.objects.get_or_create(hash=v['hash'])
+                cache.blocks.add(b)
+
+
     #_____ Methods __________
 
     def is_busy(self):
@@ -649,26 +736,54 @@ class Block(models.Model):
     PROCESSING = 'P'
     CACHED     = 'C'
     FAILED     = 'F'
+    SKIPPED    = 'S'
+    CANCELLED  = 'L'
 
     STATUS = (
         (NOT_CACHED, 'Not cached'),
         (PROCESSING, 'Processing'),
         (CACHED,     'Cached'),
         (FAILED,     'Failed'),
+        (SKIPPED,    'Skipped'),
+        (CANCELLED,  'Cancelled'),
     )
 
-    experiment              = models.ForeignKey(Experiment, related_name='blocks')
-    name                    = models.CharField(max_length=200)
-    status                  = models.CharField(max_length=1, choices=STATUS, default=NOT_CACHED)
-    analyzer                = models.BooleanField(default=False)
-    algorithm               = models.ForeignKey(Algorithm, related_name='blocks')
-    creation_date           = models.DateTimeField(null=True, blank=True, auto_now_add=True)
-    start_date              = models.DateTimeField(null=True, blank=True)
-    end_date                = models.DateTimeField(null=True, blank=True)
-    environment             = models.ForeignKey(Environment, related_name='blocks', null=True)
+    experiment = models.ForeignKey(Experiment, related_name='blocks',
+        on_delete=models.CASCADE)
+    name = models.CharField(max_length=200)
+    command = models.TextField(null=True, blank=True)
+    status = models.CharField(max_length=1, choices=STATUS, default=NOT_CACHED)
+    analyzer = models.BooleanField(default=False)
+    algorithm = models.ForeignKey(Algorithm, related_name='blocks',
+        on_delete=models.CASCADE)
+    creation_date = models.DateTimeField(null=True, blank=True,
+        auto_now_add=True)
+    runnable_date = models.DateTimeField(null=True, blank=True)
+    start_date = models.DateTimeField(null=True, blank=True)
+    end_date = models.DateTimeField(null=True, blank=True)
+    environment = models.ForeignKey(Environment, related_name='blocks',
+        null=True, on_delete=models.SET_NULL)
+    queue = models.ForeignKey(Queue, related_name='blocks', null=True,
+        on_delete=models.SET_NULL)
+
+    required_slots = models.PositiveIntegerField(default=1)
+    channel = models.CharField(max_length=200, default='', blank=True,
+        help_text="Synchronization channel within the toolchain")
+
+    # relationship to blocks to which this block depends on
+    dependencies = models.ManyToManyField('self',
+                                          related_name='dependents',
+                                          blank=True,
+                                          symmetrical=False,
+                                         )
 
     objects = BlockManager()
 
+
+    class Meta:
+        unique_together = ('experiment', 'name')
+
+
     def __str__(self):
         return self.experiment.fullname() + ', ' + self.name + ' (%s)' % self.get_status_display()
 
@@ -685,12 +800,12 @@ class Block(models.Model):
     # Accessors for statistics
 
     def __return_first__(self, field):
-        if not self.hashes.count(): return ''
-        return getattr(self.hashes.first(), field)
+        if not self.outputs.count(): return ''
+        return getattr(self.outputs.first(), field)
 
     def first_cache(self):
-        if not self.hashes.count(): return None
-        return self.hashes.first()
+        if not self.outputs.count(): return None
+        return self.outputs.first()
 
     def error_report(self):
         return self.__return_first__('error_report')
@@ -737,6 +852,9 @@ class Block(models.Model):
     def data_written_time(self):
         return self.__return_first__('data_written_time') or 0.
 
+    # Accessor for results
+    results = property(lambda self: self.__return_first__('results'))
+
 
 #----------------------------------------------------------
 
@@ -749,7 +867,7 @@ class CachedFileManager(models.Manager):
 
 class CachedFile(models.Model):
 
-    blocks = models.ManyToManyField(Block, related_name='hashes', blank=True)
+    blocks = models.ManyToManyField(Block, related_name='outputs', blank=True)
     hash  = models.CharField(max_length=64, unique=True)
 
     # the total amount of time this block took to run considering the
@@ -781,6 +899,9 @@ class CachedFile(models.Model):
     data_written_nb_blocks  = models.IntegerField(default=0)
     data_written_time       = models.FloatField(default=0.)
 
+    objects = CachedFileManager()
+
+
     def __str__(self):
         return self.hash
 
@@ -791,30 +912,60 @@ class CachedFile(models.Model):
 #----------------------------------------------------------
 
 
+class BlockInputManager(models.Manager):
+
+    def get_by_natural_key(self, hash):
+        candidate = self.filter(cache__hash=hash)
+        if candidate:
+            return candidate[0]
+        else:
+            return self.get(database__hash=hash)
+
+
+class BlockInput(models.Model):
+
+    block = models.ForeignKey(Block, related_name='inputs', null=True,
+        on_delete=models.CASCADE)
+
+    # if the input cames from another block, then this one is set
+    cache = models.ForeignKey(CachedFile, related_name='inputs', null=True,
+        on_delete=models.CASCADE)
+
+    # if the input cames from a dataset, then this one is set
+    database = models.ForeignKey(DatabaseSetOutput, related_name='blocks',
+        null=True, on_delete=models.CASCADE)
+
+    channel = models.CharField(max_length=200, default='', blank=True,
+        help_text="Synchronization channel within the toolchain")
+
+    objects = BlockInputManager()
+
+    def natural_key(self):
+        return self.has,
+
+
+#----------------------------------------------------------
+
+
 class ResultManager(models.Manager):
 
-    def get_by_natural_key(self, name, block_name, experiment_author,
-        toolchain_author, toolchain_name,
-        toolchain_version, experiment_name):
+    def get_by_natural_key(self, name, hash):
         return self.get(
             name=name,
-            block__name=block_name,
-            block__experiment__author__username=experiment_author,
-            block__experiment__toolchain__author__username=toolchain_author,
-            block__experiment__toolchain__name=toolchain_name,
-            block__experiment__toolchain__version=toolchain_version,
-            block__experiment__name=experiment_name,
+            cache__hash=hash,
             )
 
+
 class Result(models.Model):
 
     SIMPLE_TYPE_NAMES  = ('int32', 'float32', 'bool', 'string')
 
-    block        = models.ForeignKey(Block, related_name='results')
-    name         = models.CharField(max_length=200)
-    type         = models.CharField(max_length=200)
-    primary      = models.BooleanField(default=False)
-    data_value   = models.TextField(null=True, blank=True)
+    cache = models.ForeignKey(CachedFile, related_name='results', null=True,
+        on_delete=models.CASCADE)
+    name = models.CharField(max_length=200)
+    type = models.CharField(max_length=200)
+    primary = models.BooleanField(default=False)
+    data_value = models.TextField(null=True, blank=True)
 
     objects = ResultManager()
 
@@ -824,12 +975,7 @@ class Result(models.Model):
     def natural_key(self):
         return (
             self.name,
-            self.block.name,
-            self.block.experiment.author.username,
-            self.block.experiment.toolchain.author.username,
-            self.block.experiment.toolchain.name,
-            self.block.experiment.toolchain.version,
-            self.block.experiment.name,
+            self.cache.hash,
             )
 
     def value(self):