diff --git a/runbot/models/build.py b/runbot/models/build.py
index a6ceafc9..b63b915e 100644
--- a/runbot/models/build.py
+++ b/runbot/models/build.py
@@ -159,8 +159,7 @@ class BuildResult(models.Model):
     global_state = fields.Selection(make_selection(state_order), string='Status', compute='_compute_global_state', store=True, recursive=True)
     local_state = fields.Selection(make_selection(state_order), string='Build Status', default='pending', required=True, index=True)
     global_result = fields.Selection(make_selection(result_order), string='Result', compute='_compute_global_result', store=True, recursive=True)
-    local_result = fields.Selection(make_selection(result_order), string='Build Result')
-    triggered_result = fields.Selection(make_selection(result_order), string='Triggered Result')  # triggered by db only
+    local_result = fields.Selection(make_selection(result_order), string='Build Result', default='ok')
 
     requested_action = fields.Selection([('wake_up', 'To wake up'), ('deathrow', 'To kill')], string='Action requested', index=True)
     # web infos
@@ -345,9 +344,26 @@ class BuildResult(models.Model):
                     values.pop('local_result')
                 else:
                     raise ValidationError('Local result cannot be set to a less critical level')
+
+        init_global_results = self.mapped('global_result')
+        init_global_states = self.mapped('global_state')
         res = super(BuildResult, self).write(values)
-        if 'log_counter' in values:  # not 100% usefull but more correct ( see test_ir_logging)
-            self.flush()
+        for init_global_result, build in zip(init_global_results, self):
+            if init_global_result != build.global_result:
+                build._github_status()
+
+        for init_global_state, build in zip(init_global_states, self):
+            if not build.parent_id and init_global_state not in ('done', 'running') and build.global_state in ('done', 'running'):
+                build._github_status()
+
+        if values.get('global_state') in ('done', 'running'):
+            for build in self:
+                if not build.parent_id and build.global_state not in ('done', 'running'):
+                    build._github_status()
+
+
+
+
         return res
 
     def _add_child(self, param_values, orphan=False, description=False, additionnal_commit_links=False):
@@ -382,12 +398,6 @@ class BuildResult(models.Model):
             return 'warning'
         return 'ko'  # ?
 
-    def update_build_end(self):
-        for build in self:
-            build.build_end = now()
-            if build.parent_id and build.parent_id.local_state in ('running', 'done'):
-                build.parent_id.update_build_end()
-
     @api.depends('params_id.version_id.name')
     def _compute_dest(self):
         for build in self:
@@ -599,34 +609,20 @@ class BuildResult(models.Model):
         self.ensure_one()
         return '%s_%s' % (self.dest, self.active_step.name)
 
-    def _init_pendings(self, host):
-        for build in self:
-            if build.local_state != 'pending':
-                raise UserError("Build %s is not pending" % build.id)
-            if build.host != host.name:
-                raise UserError("Build %s does not have correct host" % build.id)
-            # allocate port and schedule first job
-            values = {
-                'port': self._find_port(),
-                'job_start': now(),
-                'build_start': now(),
-                'job_end': False,
-            }
-            values.update(build._next_job_values())
-            build.write(values)
-            if not build.active_step:
-                build._log('_schedule', 'No job in config, doing nothing')
-                build.local_result = 'warn'
-                continue
-            try:
-                build._log('_schedule', 'Init build environment with config %s ' % build.params_id.config_id.name)
-                os.makedirs(build._path('logs'), exist_ok=True)
-            except Exception:
-                _logger.exception('Failed initiating build %s', build.dest)
-                build._log('_schedule', 'Failed initiating build')
-                build._kill(result='ko')
-                continue
-            build._run_job()
+    def _init_pendings(self):
+        self.ensure_one()
+        build = self
+        build.port = self._find_port()
+        build.job_start = now()
+        build.build_start = now()
+        build.job_end = False
+        build._log('_schedule', 'Init build environment with config %s ' % build.params_id.config_id.name)
+        try:
+            os.makedirs(build._path('logs'), exist_ok=True)
+        except Exception:
+            _logger.exception('Failed initiating build %s', build.dest)
+            build._log('_schedule', 'Failed initiating build')
+            build._kill(result='ko')
 
     def _process_requested_actions(self):
         for build in self:
@@ -638,12 +634,15 @@ class BuildResult(models.Model):
                 continue
 
             if build.requested_action == 'wake_up':
-                if docker_state(build._get_docker_name(), build._path()) == 'RUNNING':
+                if build.local_state != 'done':
+                    build.requested_action = False
+                    build._log('wake_up', 'Impossible to wake-up, build is not done', log_type='markdown', level='SEPARATOR')
+                elif not os.path.exists(build._path()):
+                    build.requested_action = False
+                    build._log('wake_up', 'Impossible to wake-up, **build dir does not exists anymore**', log_type='markdown', level='SEPARATOR')
+                elif docker_state(build._get_docker_name(), build._path()) == 'RUNNING':
                     build.write({'requested_action': False, 'local_state': 'running'})
                     build._log('wake_up', 'Waking up failed, **docker is already running**', log_type='markdown', level='SEPARATOR')
-                elif not os.path.exists(build._path()):
-                    build.write({'requested_action': False, 'local_state': 'done'})
-                    build._log('wake_up', 'Impossible to wake-up, **build dir does not exists anymore**', log_type='markdown', level='SEPARATOR')
                 else:
                     try:
                         log_path = build._path('logs', 'wake_up.txt')
@@ -674,45 +673,39 @@ class BuildResult(models.Model):
     def _schedule(self):
         """schedule the build"""
         icp = self.env['ir.config_parameter'].sudo()
-        hosts_by_name = {h.name: h for h in self.env['runbot.host'].search([('name', 'in', self.mapped('host'))])}
-        hosts_by_build = {b.id: hosts_by_name[b.host] for b in self}
-        for build in self:
-            if build.local_state not in ['testing', 'running']:
-                raise UserError("Build %s is not testing/running: %s" % (build.id, build.local_state))
-            if build.local_state == 'testing':
-                # failfast in case of docker error (triggered in database)
-                if build.triggered_result and not build.active_step.ignore_triggered_result:
-                    worst_result = self._get_worst_result([build.triggered_result, build.local_result])
-                    if worst_result != build.local_result:
-                        build.local_result = build.triggered_result
-                        build._github_status()  # failfast
-            # check if current job is finished
+        self.ensure_one()
+        build = self
+        if build.local_state not in ['testing', 'running', 'pending']:
+            return False
+        # check if current job is finished
+        if build.local_state == 'pending':
+            build._init_pendings()
+        else:
             _docker_state = docker_state(build._get_docker_name(), build._path())
             if _docker_state == 'RUNNING':
                 timeout = min(build.active_step.cpu_limit, int(icp.get_param('runbot.runbot_timeout', default=10000)))
                 if build.local_state != 'running' and build.job_time > timeout:
                     build._log('_schedule', '%s time exceeded (%ss)' % (build.active_step.name if build.active_step else "?", build.job_time))
                     build._kill(result='killed')
-                continue
+                return False
             elif _docker_state in ('UNKNOWN', 'GHOST') and (build.local_state == 'running' or build.active_step._is_docker_step()):  # todo replace with docker_start
                 docker_time = time.time() - dt2time(build.docker_start or build.job_start)
                 if docker_time < 5:
-                    continue
+                    return False
                 elif docker_time < 60:
                     _logger.info('container "%s" seems too take a while to start :%s' % (build.job_time, build._get_docker_name()))
-                    continue
+                    return False
                 else:
                     build._log('_schedule', 'Docker with state %s not started after 60 seconds, skipping' % _docker_state, level='ERROR')
-            if hosts_by_build[build.id]._fetch_local_logs(build_ids=build.ids):
-                continue  # avoid to make results with remaining logs
+            if self.env['runbot.host']._fetch_local_logs(build_ids=build.ids):
+                return True  # avoid to make results with remaining logs
             # No job running, make result and select next job
-            build_values = {
-                'job_end': now(),
-                'docker_start': False,
-            }
+
+            build.job_end = now()
+            build.docker_start = False
             # make result of previous job
             try:
-                results = build.active_step._make_results(build)
+                build.active_step._make_results(build)
             except Exception as e:
                 if isinstance(e, RunbotException):
                     message = e.args[0][:300000]
@@ -720,50 +713,71 @@ class BuildResult(models.Model):
                     message = 'An error occured while computing results of %s:\n %s' % (build.job, str(e).replace('\\n', '\n').replace("\\'", "'")[:10000])
                     _logger.exception(message)
                 build._log('_make_results', message, level='ERROR')
-                results = {'local_result': 'ko'}
-
-            build_values.update(results)
+                build.local_result = 'ko'
 
             # compute statistics before starting next job
             build.active_step._make_stats(build)
-
             build.active_step.log_end(build)
 
-            build_values.update(build._next_job_values())  # find next active_step or set to done
+        step_ids = self.params_id.config_id.step_ids()
+        if not step_ids:  # no job to do, build is done
+            self.active_step = False
+            self.local_state = 'done'
+            build._log('_schedule', 'No job in config, doing nothing')
+            build.local_result = 'warn'
+            return False
 
+        if not self.active_step and self.local_state != 'pending':  # wakeup docker finished
+            build.active_step = False
+            build.local_state = 'done'
+            return False
 
-            ending_build = build.local_state not in ('done', 'running') and build_values.get('local_state') in ('done', 'running')
-            if ending_build:
-                build.update_build_end()
+        if not self.active_step:
+            next_index = 0
+        else:
+            if self.active_step not in step_ids:
+                self._log('run', 'Config was modified and current step does not exists anymore, skipping.', level='ERROR')
+                self.active_step = False
+                self.local_state = 'done'
+                self.local_result = 'ko'
+                return False
+            next_index = step_ids.index(self.active_step) + 1
 
-            build.write(build_values)
-            if ending_build:
-                if not build.local_result:  # Set 'ok' result if no result set (no tests job on build)
-                    build.local_result = 'ok'
-                    build._logger("No result set, setting ok by default")
-                build._github_status()
-            build._run_job()
+        while True:
+            if next_index >= len(step_ids):  # final job, build is done
+                self.active_step = False
+                self.local_state = 'done'
+                return False
+            new_step = step_ids[next_index]  # job to do, state is job_state (testing or running)
+            if new_step.domain_filter and not self.filtered_domain(safe_eval(new_step.domain_filter)):
+                self._log('run', '**Skipping** step ~~%s~~ from config **%s**' % (new_step.name, self.params_id.config_id.name), log_type='markdown', level='SEPARATOR')
+                next_index += 1
+                continue
+            break
+        build.active_step = new_step.id
+        build.local_state = new_step._step_state()
 
+        return build._run_job()
 
     def _run_job(self):
-        # run job
-        for build in self:
-            if build.local_state != 'done':
-                build._logger('running %s', build.active_step.name)
-                os.makedirs(build._path('logs'), exist_ok=True)
-                os.makedirs(build._path('datadir'), exist_ok=True)
-                try:
-                    build.active_step._run(build)  # run should be on build?
-                except TransactionRollbackError:
-                    raise
-                except Exception as e:
-                    if isinstance(e, RunbotException):
-                        message = e.args[0]
-                    else:
-                        message = '%s failed running step %s:\n %s' % (build.dest, build.job, str(e).replace('\\n', '\n').replace("\\'", "'"))
-                    _logger.exception(message)
-                    build._log("run", message, level='ERROR')
-                    build._kill(result='ko')
+        self.ensure_one()
+        build = self
+        if build.local_state != 'done':
+            build._logger('running %s', build.active_step.name)
+            os.makedirs(build._path('logs'), exist_ok=True)
+            os.makedirs(build._path('datadir'), exist_ok=True)
+            try:
+                return build.active_step._run(build)  # run should be on build?
+            except TransactionRollbackError:
+                raise
+            except Exception as e:
+                if isinstance(e, RunbotException):
+                    message = e.args[0]
+                else:
+                    message = '%s failed running step %s:\n %s' % (build.dest, build.job, str(e).replace('\\n', '\n').replace("\\'", "'"))
+                _logger.exception(message)
+                build._log("run", message, level='ERROR')
+                build._kill(result='ko')
 
     def _docker_run(self, cmd=None, ro_volumes=None, **kwargs):
         self.ensure_one()
@@ -793,7 +807,10 @@ class BuildResult(models.Model):
         user = getpass.getuser()
         ro_volumes[f'/home/{user}/.odoorc'] = self._path('.odoorc')
         kwargs.pop('build_dir', False)  # todo check python steps
-        docker_run(cmd=cmd, build_dir=self._path(), ro_volumes=ro_volumes, **kwargs)
+        build_dir = self._path()
+        def start_docker():
+            docker_run(cmd=cmd, build_dir=build_dir, ro_volumes=ro_volumes, **kwargs)
+        return start_docker
 
     def _path(self, *l, **kw):
         """Return the repo build path"""
@@ -1061,36 +1078,6 @@ class BuildResult(models.Model):
                 'build_id': self.id
             })
 
-    def _next_job_values(self):
-        self.ensure_one()
-        step_ids = self.params_id.config_id.step_ids()
-        if not step_ids:  # no job to do, build is done
-            return {'active_step': False, 'local_state': 'done'}
-
-        if not self.active_step and self.local_state != 'pending':
-            # means that a step has been run manually without using config
-            return {'active_step': False, 'local_state': 'done'}
-
-        if not self.active_step:
-            next_index = 0
-        else:
-            if self.active_step not in step_ids:
-                self._log('run', 'Config was modified and current step does not exists anymore, skipping.', level='ERROR')
-                return {'active_step': False, 'local_state': 'done', 'local_result': self._get_worst_result([self.local_result, 'ko'])}
-            next_index = step_ids.index(self.active_step) + 1
-
-        while True:
-            if next_index >= len(step_ids):  # final job, build is done
-                return {'active_step': False, 'local_state': 'done'}
-            new_step = step_ids[next_index]  # job to do, state is job_state (testing or running)
-            if new_step.domain_filter and not self.filtered_domain(safe_eval(new_step.domain_filter)):
-
-                self._log('run', '**Skipping** step ~~%s~~ from config **%s**' % (new_step.name, self.params_id.config_id.name), log_type='markdown', level='SEPARATOR')
-                next_index += 1
-                continue
-            break
-        return {'active_step': new_step.id, 'local_state': new_step._step_state()}
-
     def _get_py_version(self):
         """return the python name to use from build batch"""
         (server_commit, server_file) = self._get_server_info()
diff --git a/runbot/models/build_config.py b/runbot/models/build_config.py
index 53cf33e4..c2941511 100644
--- a/runbot/models/build_config.py
+++ b/runbot/models/build_config.py
@@ -166,7 +166,6 @@ class ConfigStep(models.Model):
     # python
     python_code = fields.Text('Python code', tracking=True, default=PYTHON_DEFAULT)
     python_result_code = fields.Text('Python code for result', tracking=True, default=PYTHON_DEFAULT)
-    ignore_triggered_result = fields.Boolean('Ignore error triggered in logs', tracking=True, default=False)
     running_job = fields.Boolean('Job final state is running', default=False, help="Docker won't be killed if checked")
     # create_build
     create_config_ids = fields.Many2many('runbot.build.config', 'runbot_build_config_step_ids_create_config_ids_rel', string='New Build Configs', tracking=True, index=True)
@@ -275,14 +274,14 @@ class ConfigStep(models.Model):
             url = f"{log_url}/runbot/static/build/{build.dest}/logs/{self.name}.txt"
             log_link = f'[@icon-file-text]({url})'
         build._log('run', 'Starting step **%s** from config **%s** %s' % (self.name, build.params_id.config_id.name, log_link), log_type='markdown', level='SEPARATOR')
-        self._run_step(build, log_path)
+        return self._run_step(build, log_path)
 
     def _run_step(self, build, log_path, **kwargs):
         build.log_counter = self.env['ir.config_parameter'].sudo().get_param('runbot.runbot_maxlogs', 100)
         run_method = getattr(self, '_run_%s' % self.job_type)
         docker_params = run_method(build, log_path, **kwargs)
         if docker_params:
-            build._docker_run(**docker_params)
+            return build._docker_run(**docker_params)
 
     def _run_create_build(self, build, log_path):
         count = 0
@@ -906,23 +905,20 @@ class ConfigStep(models.Model):
         return ['--omit', ','.join(pattern_to_omit)]
 
     def _make_results(self, build):
-        build_values = {}
         log_time = self._get_log_last_write(build)
         if log_time:
-            build_values['job_end'] = log_time
+            build.job_end = log_time
         if self.job_type == 'python' and self.python_result_code and self.python_result_code != PYTHON_DEFAULT:
-            build_values.update(self._make_python_results(build))
+            build.write(self._make_python_results(build))
         elif self.job_type in ['install_odoo', 'python']:
             if self.coverage:
-                build_values.update(self._make_coverage_results(build))
+                build.write(self._make_coverage_results(build))
             if self.test_enable or self.test_tags:
-                build_values.update(self._make_tests_results(build))
+                build.write(self._make_tests_results(build))
         elif self.job_type == 'test_upgrade':
-            build_values.update(self._make_upgrade_results(build))
+            build.write(self._make_upgrade_results(build))
         elif self.job_type == 'restore':
-            build_values.update(self._make_restore_results(build))
-
-        return build_values
+            build.write(self._make_restore_results(build))
 
     def _make_python_results(self, build):
         eval_ctx = self.make_python_ctx(build)
diff --git a/runbot/models/bundle.py b/runbot/models/bundle.py
index 7e28787e..4c86d3cf 100644
--- a/runbot/models/bundle.py
+++ b/runbot/models/bundle.py
@@ -55,7 +55,6 @@ class Bundle(models.Model):
     # extra_info
     for_next_freeze = fields.Boolean('Should be in next freeze')
 
-
     @api.depends('name')
     def _compute_host_id(self):
         assigned_only = None
diff --git a/runbot/models/event.py b/runbot/models/event.py
index a771ce47..d4c18233 100644
--- a/runbot/models/event.py
+++ b/runbot/models/event.py
@@ -36,10 +36,11 @@ class runbot_event(models.Model):
             build_logs = logs_by_build_id[build.id]
             for ir_log in build_logs:
                 ir_log['active_step_id'] = build.active_step.id
-                if ir_log['level'].upper() == 'WARNING':
-                    build.triggered_result = 'warn'
-                elif ir_log['level'].upper() == 'ERROR':
-                    build.triggered_result = 'ko'
+                if build.local_state != 'running':
+                    if ir_log['level'].upper() == 'WARNING':
+                        build.local_result = 'warn'
+                    elif ir_log['level'].upper() == 'ERROR':
+                        build.local_result = 'ko'
         return super().create(vals_list)
 
     def _markdown(self):
diff --git a/runbot/models/host.py b/runbot/models/host.py
index ff92d0dd..1b42db8f 100644
--- a/runbot/models/host.py
+++ b/runbot/models/host.py
@@ -209,7 +209,7 @@ class Host(models.Model):
                 res.append({name:value for name, value in zip(col_names, row)})
             return res
 
-    def process_logs(self, build_ids=None):
+    def _process_logs(self, build_ids=None):
         """move logs from host to the leader"""
         ir_logs = self._fetch_local_logs()
         logs_by_build_id = defaultdict(list)
@@ -253,6 +253,13 @@ class Host(models.Model):
             with local_pg_cursor(logs_db_name) as local_cr:
                 local_cr.execute("DELETE FROM ir_logging WHERE id in %s", [tuple(local_log_ids)])
 
+    def get_build_domain(self, domain=None):
+        domain = domain or []
+        return [('host', '=', self.name)] + domain
+
+    def get_builds(self, domain, order=None):
+        return self.env['runbot.build'].search(self.get_build_domain(domain), order=order)
+
     def _process_messages(self):
         self.host_message_ids._process()
 
diff --git a/runbot/models/runbot.py b/runbot/models/runbot.py
index 1b967f34..445134a6 100644
--- a/runbot/models/runbot.py
+++ b/runbot/models/runbot.py
@@ -39,80 +39,76 @@ class Runbot(models.AbstractModel):
     def _scheduler(self, host):
         self._gc_testing(host)
         self._commit()
-        for build in self._get_builds_with_requested_actions(host):
-            build = build.browse(build.id)  # remove preftech ids, manage build one by one
+        processed = 0
+        for build in host.get_builds([('requested_action', 'in', ['wake_up', 'deathrow'])]):
+            processed += 1
             build._process_requested_actions()
             self._commit()
-        host.process_logs()
+        host._process_logs()
         self._commit()
         host._process_messages()
         self._commit()
-        for build in self._get_builds_to_schedule(host):
+        for build in host.get_builds([('local_state', 'in', ['testing', 'running'])]) | self._get_builds_to_init(host):
             build = build.browse(build.id)  # remove preftech ids, manage build one by one
-            build._schedule()
+            result = build._schedule()
+            if result:
+                processed += 1
             self._commit()
-        self._assign_pending_builds(host, host.nb_worker, [('build_type', '!=', 'scheduled')])
+            if callable(result):
+                result()  # start docker
+        processed += self._assign_pending_builds(host, host.nb_worker, [('build_type', '!=', 'scheduled')])
         self._commit()
-        self._assign_pending_builds(host, host.nb_worker - 1 or host.nb_worker)
+        processed += self._assign_pending_builds(host, host.nb_worker - 1 or host.nb_worker)
         self._commit()
-        self._assign_pending_builds(host, host.nb_worker and host.nb_worker + 1, [('build_type', '=', 'priority')])
+        processed += self._assign_pending_builds(host, host.nb_worker and host.nb_worker + 1, [('build_type', '=', 'priority')])
         self._commit()
-        for build in self._get_builds_to_init(host):
-            build = build.browse(build.id)  # remove preftech ids, manage build one by one
-            build._init_pendings(host)
-            self._commit()
         self._gc_running(host)
         self._commit()
         self._reload_nginx()
-
-    def build_domain_host(self, host, domain=None):
-        domain = domain or []
-        return [('host', '=', host.name)] + domain
-
-    def _get_builds_with_requested_actions(self, host):
-        return self.env['runbot.build'].search(self.build_domain_host(host, [('requested_action', 'in', ['wake_up', 'deathrow'])]))
-
-    def _get_builds_to_schedule(self, host):
-        return self.env['runbot.build'].search(self.build_domain_host(host, [('local_state', 'in', ['testing', 'running'])]))
+        self._commit()
+        return processed
 
     def _assign_pending_builds(self, host, nb_worker, domain=None):
         if host.assigned_only or nb_worker <= 0:
-            return
-        domain_host = self.build_domain_host(host)
-        reserved_slots = self.env['runbot.build'].search_count(domain_host + [('local_state', 'in', ('testing', 'pending'))])
+            return 0
+        reserved_slots = len(host.get_builds([('local_state', 'in', ('testing', 'pending'))]))
         assignable_slots = (nb_worker - reserved_slots)
         if assignable_slots > 0:
             allocated = self._allocate_builds(host, assignable_slots, domain)
             if allocated:
                 _logger.info('Builds %s where allocated to runbot', allocated)
+            return len(allocated)
+        return 0
 
     def _get_builds_to_init(self, host):
-        domain_host = self.build_domain_host(host)
-        used_slots = self.env['runbot.build'].search_count(domain_host + [('local_state', '=', 'testing')])
+        domain_host = host.get_build_domain()
+        used_slots = len(host.get_builds([('local_state', '=', 'testing')]))
         available_slots = host.nb_worker - used_slots
-        if available_slots <= 0:
-            return self.env['runbot.build']
-        return self.env['runbot.build'].search(domain_host + [('local_state', '=', 'pending')], limit=available_slots)
+        build_to_init = self.env['runbot.build']
+        if available_slots > 0:
+            build_to_init |= self.env['runbot.build'].search(domain_host + [('local_state', '=', 'pending')], limit=available_slots)
+        if available_slots + 1 > 0:
+            build_to_init |= self.env['runbot.build'].search(domain_host + [('local_state', '=', 'pending'), ('build_type', '=', 'priority')], limit=1)
+        return build_to_init
 
     def _gc_running(self, host):
         running_max = host.get_running_max()
-        domain_host = self.build_domain_host(host)
         Build = self.env['runbot.build']
-        cannot_be_killed_ids = Build.search(domain_host + [('keep_running', '=', True)]).ids
+        cannot_be_killed_ids = host.get_builds([('keep_running', '=', True)]).ids
         sticky_bundles = self.env['runbot.bundle'].search([('sticky', '=', True), ('project_id.keep_sticky_running', '=', True)])
         cannot_be_killed_ids += [
             build.id
             for build in sticky_bundles.mapped('last_batchs.slot_ids.build_id')
             if build.host == host.name
         ][:running_max]
-        build_ids = Build.search(domain_host + [('local_state', '=', 'running'), ('id', 'not in', cannot_be_killed_ids)], order='job_start desc').ids
+        build_ids = host.get_builds([('local_state', '=', 'running'), ('id', 'not in', cannot_be_killed_ids)], order='job_start desc').ids
         Build.browse(build_ids)[running_max:]._kill()
 
     def _gc_testing(self, host):
         """garbage collect builds that could be killed"""
         # decide if we need room
         Build = self.env['runbot.build']
-        domain_host = self.build_domain_host(host)
+        domain_host = host.get_build_domain()
         testing_builds = Build.search(domain_host + [('local_state', 'in', ['testing', 'pending']), ('requested_action', '!=', 'deathrow')])
         used_slots = len(testing_builds)
         available_slots = host.nb_worker - used_slots
@@ -282,11 +278,11 @@ class Runbot(models.AbstractModel):
 
         return manager.get('sleep', default_sleep)
 
-    def _scheduler_loop_turn(self, host, default_sleep=5):
-        _logger.info('Scheduling...')
+    def _scheduler_loop_turn(self, host, sleep=5):
         with self.manage_host_exception(host) as manager:
-            self._scheduler(host)
-        return manager.get('sleep', default_sleep)
+            if self._scheduler(host):
+                sleep = 0.1
+        return manager.get('sleep', sleep)
 
     @contextmanager
     def manage_host_exception(self, host):
diff --git a/runbot/tests/common.py b/runbot/tests/common.py
index 67b86c5a..57324dea 100644
--- a/runbot/tests/common.py
+++ b/runbot/tests/common.py
@@ -181,7 +181,6 @@ class RunbotCase(TransactionCase):
         self.start_patcher('docker_stop', 'odoo.addons.runbot.container._docker_stop')
         self.start_patcher('docker_get_gateway_ip', 'odoo.addons.runbot.models.build_config.docker_get_gateway_ip', None)
 
-        self.start_patcher('cr_commit', 'odoo.sql_db.Cursor.commit', None)
         self.start_patcher('repo_commit', 'odoo.addons.runbot.models.runbot.Runbot._commit', None)
         self.start_patcher('_local_cleanup_patcher', 'odoo.addons.runbot.models.build.BuildResult._local_cleanup')
         self.start_patcher('_local_pg_dropdb_patcher', 'odoo.addons.runbot.models.build.BuildResult._local_pg_dropdb')
@@ -194,6 +193,11 @@ class RunbotCase(TransactionCase):
 
         self.start_patcher('_get_py_version', 'odoo.addons.runbot.models.build.BuildResult._get_py_version', 3)
 
+        def no_commit(*_args, **_kwargs):
+            _logger.info('Skipping commit')
+
+        self.patch(self.env.cr, 'commit', no_commit)
+
 
     def start_patcher(self, patcher_name, patcher_path, return_value=DEFAULT, side_effect=DEFAULT, new=DEFAULT):
 
diff --git a/runbot/tests/test_build.py b/runbot/tests/test_build.py
index d1fa191a..d977904c 100644
--- a/runbot/tests/test_build.py
+++ b/runbot/tests/test_build.py
@@ -4,8 +4,10 @@ import datetime
 from unittest.mock import patch
 
 from odoo import fields
+from odoo.tests import tagged
 from odoo.exceptions import UserError, ValidationError
 from .common import RunbotCase, RunbotCaseMinimalSetup
+from unittest.mock import MagicMock
 
 
 def rev_parse(repo, branch_name):
@@ -18,6 +20,7 @@ def rev_parse(repo, branch_name):
     return head_hash
 
 
+@tagged('-at_install', 'post_istall')
 class TestBuildParams(RunbotCaseMinimalSetup):
 
     def setUp(self):
@@ -171,7 +174,10 @@ class TestBuildResult(RunbotCase):
         # test a bulk write, that one cannot change from 'ko' to 'ok'
         builds = self.Build.browse([build.id, other.id])
         with self.assertRaises(ValidationError):
-            builds.write({'local_result': 'ok'})
+            builds.write({'local_result': 'warn'})
+        # self.assertEqual(build.local_result, 'warn')
+        # self.assertEqual(other.local_result, 'ko')
+
 
     def test_markdown_description(self):
         build = self.Build.create({
@@ -331,6 +337,11 @@ class TestBuildResult(RunbotCase):
         build1 = self.Build.create({
             'params_id': self.server_params.id,
         })
+        self.assertEqual('pending', build1.global_state)
+
+        build1.local_state = 'testing'
+        self.assertEqual('testing', build1.global_state)
+
         build1_1 = self.Build.create({
             'params_id': self.server_params.id,
             'parent_id': build1.id,
@@ -339,6 +350,15 @@ class TestBuildResult(RunbotCase):
             'params_id': self.server_params.id,
             'parent_id': build1.id,
         })
+        self.assertEqual('testing', build1.global_state)
+        self.assertEqual('pending', build1_1.global_state)
+        self.assertEqual('pending', build1_2.global_state)
+
+        build1_1.local_state = 'testing'
+        self.assertEqual('testing', build1.global_state)
+        self.assertEqual('testing', build1_1.global_state)
+        self.assertEqual('pending', build1_2.global_state)
+
         build1_1_1 = self.Build.create({
             'params_id': self.server_params.id,
             'parent_id': build1_1.id,
@@ -348,60 +368,132 @@ class TestBuildResult(RunbotCase):
             'parent_id': build1_1.id,
         })
 
-        def assert_state(global_state, build):
-            self.assertEqual(build.global_state, global_state)
+        self.assertEqual('testing', build1.global_state)
+        self.assertEqual('testing', build1_1.global_state)
+        self.assertEqual('pending', build1_2.global_state)
+        self.assertEqual('pending', build1_1_1.global_state)
+        self.assertEqual('pending', build1_1_2.global_state)
 
-        assert_state('pending', build1)
-        assert_state('pending', build1_1)
-        assert_state('pending', build1_2)
-        assert_state('pending', build1_1_1)
-        assert_state('pending', build1_1_2)
+        build1_2.flush()
+        with self.assertQueries(['''UPDATE "runbot_build" SET "global_state"=%s,"local_state"=%s,"write_date"=%s,"write_uid"=%s WHERE id IN %s''']):
+            build1_2.local_state = "testing"
+            build1_2.flush()
 
-        build1.local_state = 'testing'
-        build1_1.local_state = 'testing'
+        self.assertEqual('testing', build1.global_state)
+        self.assertEqual('testing', build1_2.global_state)
+
+        with self.assertQueries([]):  # no change should be triggered
+            build1_2.local_state = "testing"
+
+        # with self.assertQueries(['''UPDATE "runbot_build" SET "global_state"=%s,"local_state"=%s,"write_date"=%s,"write_uid"=%s WHERE id IN %s''']):
         build1.local_state = 'done'
+        build1.flush()
+
+        self.assertEqual('waiting', build1.global_state)
+        self.assertEqual('testing', build1_1.global_state)
+
+        # with self.assertQueries([]): # write the same value, no update should be triggered
+        build1.local_state = 'done'
+        build1.flush()
+
         build1_1.local_state = 'done'
 
-        assert_state('waiting', build1)
-        assert_state('waiting', build1_1)
-        assert_state('pending', build1_2)
-        assert_state('pending', build1_1_1)
-        assert_state('pending', build1_1_2)
+        self.assertEqual('waiting', build1.global_state)
+        self.assertEqual('waiting', build1_1.global_state)
+        self.assertEqual('testing', build1_2.global_state)
+        self.assertEqual('pending', build1_1_1.global_state)
+        self.assertEqual('pending', build1_1_2.global_state)
 
         build1_1_1.local_state = 'testing'
 
-        assert_state('waiting', build1)
-        assert_state('waiting', build1_1)
-        assert_state('pending', build1_2)
-        assert_state('testing', build1_1_1)
-        assert_state('pending', build1_1_2)
+        self.assertEqual('waiting', build1.global_state)
+        self.assertEqual('waiting', build1_1.global_state)
+        self.assertEqual('testing', build1_2.global_state)
+        self.assertEqual('testing', build1_1_1.global_state)
+        self.assertEqual('pending', build1_1_2.global_state)
 
-        build1_2.local_state = 'testing'
+        with self.assertQueries([]):
+            build1_2.local_state = 'testing'
 
-        assert_state('waiting', build1)
-        assert_state('waiting', build1_1)
-        assert_state('testing', build1_2)
-        assert_state('testing', build1_1_1)
-        assert_state('pending', build1_1_2)
+        self.assertEqual('waiting', build1.global_state)
+        self.assertEqual('waiting', build1_1.global_state)
+        self.assertEqual('testing', build1_2.global_state)
+        self.assertEqual('testing', build1_1_1.global_state)
+        self.assertEqual('pending', build1_1_2.global_state)
 
-        build1_2.local_state = 'testing'  # writing same state a second time
+        build1_2.local_state = 'done'
+        build1_1_1.local_state = 'done'
+        build1_1_2.local_state = 'testing'
 
-        assert_state('waiting', build1)
-        assert_state('waiting', build1_1)
-        assert_state('testing', build1_2)
-        assert_state('testing', build1_1_1)
-        assert_state('pending', build1_1_2)
+        self.assertEqual('waiting', build1.global_state)
+        self.assertEqual('waiting', build1_1.global_state)
+        self.assertEqual('done', build1_2.global_state)
+        self.assertEqual('done', build1_1_1.global_state)
+        self.assertEqual('testing', build1_1_2.global_state)
 
         build1_1_2.local_state = 'done'
+
+        self.assertEqual('done', build1.global_state)
+        self.assertEqual('done', build1_1.global_state)
+        self.assertEqual('done', build1_2.global_state)
+        self.assertEqual('done', build1_1_1.global_state)
+        self.assertEqual('done', build1_1_2.global_state)
+
+    def test_rebuild_sub_sub_build(self):
+        build1 = self.Build.create({
+            'params_id': self.server_params.id,
+        })
+        build1.local_state = 'testing'
+        build1_1 = self.Build.create({
+            'params_id': self.server_params.id,
+            'parent_id': build1.id,
+        })
+        build1_1.local_state = 'testing'
+        build1.local_state = 'done'
+        build1_1_1 = self.Build.create({
+            'params_id': self.server_params.id,
+            'parent_id': build1_1.id,
+        })
+        build1_1_1.local_state = 'testing'
+        build1_1.local_state = 'done'
+        self.assertEqual('waiting', build1.global_state)
+        self.assertEqual('waiting', build1_1.global_state)
+        self.assertEqual('testing', build1_1_1.global_state)
+
+        build1_1_1.local_result = 'ko'
         build1_1_1.local_state = 'done'
-        build1_2.local_state = 'done'
+        self.assertEqual('done', build1.global_state)
+        self.assertEqual('done', build1_1.global_state)
+        self.assertEqual('done', build1_1_1.global_state)
+        self.assertEqual('ko', build1.global_result)
+        self.assertEqual('ko', build1_1.global_result)
+        self.assertEqual('ko', build1_1_1.global_result)
 
-        assert_state('done', build1)
-        assert_state('done', build1_1)
-        assert_state('done', build1_2)
-        assert_state('done', build1_1_1)
-        assert_state('done', build1_1_2)
+        rebuild1_1_1 = self.Build.create({  # this is a rebuild
+            'params_id': self.server_params.id,
+            'parent_id': build1_1.id,
+        })
+        build1_1_1.orphan_result = True
 
+        self.assertEqual('ok', build1.global_result)
+        self.assertEqual('ok', build1_1.global_result)
+        self.assertEqual('ko', build1_1_1.global_result)
+        self.assertEqual('waiting', build1.global_state)
+        self.assertEqual('waiting', build1_1.global_state)
+        self.assertEqual('done', build1_1_1.global_state)
+        self.assertEqual('pending', rebuild1_1_1.global_state)
+
+        rebuild1_1_1.local_result = 'ok'
+        rebuild1_1_1.local_state = 'done'
+
+        self.assertEqual('ok', build1.global_result)
+        self.assertEqual('ok', build1_1.global_result)
+        self.assertEqual('ko', build1_1_1.global_result)
+        self.assertEqual('ok', rebuild1_1_1.global_result)
+        self.assertEqual('done', build1.global_state)
+        self.assertEqual('done', build1_1.global_state)
+        self.assertEqual('done', build1_1_1.global_state)
+        self.assertEqual('done', rebuild1_1_1.global_state)
 
 class TestGc(RunbotCaseMinimalSetup):
 
@@ -446,6 +538,7 @@ class TestGc(RunbotCaseMinimalSetup):
         build_a.write({'local_state': 'testing', 'host': host.name})
         build_b.write({'local_state': 'testing', 'host': 'runbot_yyy'})
 
+
         # no room needed, verify that nobody got killed
         self.Runbot._gc_testing(host)
         self.assertFalse(build_a.requested_action)
@@ -487,3 +580,34 @@ class TestGc(RunbotCaseMinimalSetup):
         self.assertFalse(build_b.requested_action)
         self.assertFalse(build_a_last.requested_action)
         self.assertFalse(children_b.requested_action)
+
+
+class TestGithubStatus(RunbotCase):
+
+    def setUp(self):
+        super().setUp()
+        self.build = self.Build.create({
+            'params_id': self.base_params.id,
+            'description': 'A nice **description**'
+        })
+
+    def test_change_state(self):
+        self.callcount = 0
+
+        def github_status(build):
+            self.callcount += 1
+
+        with patch('odoo.addons.runbot.models.build.BuildResult._github_status', github_status):
+            self.callcount = 0
+            self.build.local_state = 'testing'
+
+            self.assertEqual(self.callcount, 0, "_github_status shouldn't have been called")
+
+            self.callcount = 0
+            self.build.local_state = 'running'
+
+            self.assertEqual(self.callcount, 1, "_github_status should have been called")
+
+            self.callcount = 0
+            self.build.local_state = 'done'
+            self.assertEqual(self.callcount, 0, "_github_status shouldn't have been called")
diff --git a/runbot/tests/test_build_config_step.py b/runbot/tests/test_build_config_step.py
index 1053a098..6b7c588b 100644
--- a/runbot/tests/test_build_config_step.py
+++ b/runbot/tests/test_build_config_step.py
@@ -214,7 +214,6 @@ class TestBuildConfigStepCreate(TestBuildConfigStepCommon):
     def test_config_step_create_results(self):
         """ Test child builds are taken into account"""
 
-
         self.config_step._run_create_build(self.parent_build, '/tmp/essai')
         self.assertEqual(len(self.parent_build.children_ids), 2, 'Two sub-builds should have been generated')
 
@@ -224,6 +223,7 @@ class TestBuildConfigStepCreate(TestBuildConfigStepCommon):
             child_build.local_result = 'ko'
             self.assertEqual(child_build.global_result, 'ko')
 
+
         self.assertEqual(self.parent_build.global_result, 'ko')
 
     def test_config_step_create(self):
@@ -236,6 +236,7 @@ class TestBuildConfigStepCreate(TestBuildConfigStepCommon):
         for child_build in self.parent_build.children_ids:
             self.assertTrue(child_build.orphan_result, 'An orphan result config step should mark the build as orphan_result')
             child_build.local_result = 'ko'
+            # child_build._update_:globals()
 
         self.assertEqual(self.parent_build.global_result, 'ok')
 
@@ -455,19 +456,19 @@ class TestBuildConfigStep(TestBuildConfigStepCommon):
 
         self.patchers['docker_run'].side_effect = docker_run
 
-        config_step._run_step(self.parent_build, 'dev/null/logpath')
+        config_step._run_step(self.parent_build, 'dev/null/logpath')()
 
         assert_db_name = 'custom_build'
         parent_build_params = self.parent_build.params_id.copy({'config_data': {'db_name': 'custom_build'}})
         parent_build = self.parent_build.copy({'params_id': parent_build_params.id})
-        config_step._run_step(parent_build, 'dev/null/logpath')
+        config_step._run_step(parent_build, 'dev/null/logpath')()
 
         config_step = self.ConfigStep.create({
             'name': 'run_test',
             'job_type': 'run_odoo',
             'custom_db_name': 'custom',
         })
-        config_step._run_step(parent_build, 'dev/null/logpath')
+        config_step._run_step(parent_build, 'dev/null/logpath')()
 
         self.assertEqual(call_count, 3)
 
@@ -489,7 +490,7 @@ docker_params = dict(cmd=cmd)
             self.assertIn('-d test_database', run_cmd)
 
         self.patchers['docker_run'].side_effect = docker_run
-        config_step._run_step(self.parent_build, 'dev/null/logpath')
+        config_step._run_step(self.parent_build, 'dev/null/logpath')()
         self.patchers['docker_run'].assert_called_once()
         db = self.env['runbot.database'].search([('name', '=', 'test_database')])
         self.assertEqual(db.build_id, self.parent_build)
@@ -525,7 +526,7 @@ def run():
             call_count += 1
 
         self.patchers['docker_run'].side_effect = docker_run
-        config_step._run_step(self.parent_build, 'dev/null/logpath')
+        config_step._run_step(self.parent_build, 'dev/null/logpath')()
 
         self.assertEqual(call_count, 1)
 
@@ -564,10 +565,14 @@ Initiating shutdown
         })
         logs = []
         with patch('builtins.open', mock_open(read_data=file_content)):
-            result = config_step._make_results(build)
-        self.assertEqual(result, {'job_end': '1970-01-01 02:00:00', 'local_result': 'ok'})
+            config_step._make_results(build)
+        self.assertEqual(str(build.job_end), '1970-01-01 02:00:00')
         self.assertEqual(logs, [('INFO', 'Getting results for build %s' % build.dest)])
+        self.assertEqual(build.local_result, 'ok')
         # no shutdown
+        build = self.Build.create({
+            'params_id': self.base_params.id,
+        })
         logs = []
         file_content = """
 Loading stuff
@@ -575,26 +580,34 @@ odoo.stuff.modules.loading: Modules loaded.
 Some post install stuff
         """
         with patch('builtins.open', mock_open(read_data=file_content)):
-            result = config_step._make_results(build)
-        self.assertEqual(result, {'job_end': '1970-01-01 02:00:00', 'local_result': 'ko'})
+            config_step._make_results(build)
+        self.assertEqual(str(build.job_end), '1970-01-01 02:00:00')
+        self.assertEqual(build.local_result, 'ko')
         self.assertEqual(logs, [
             ('INFO', 'Getting results for build %s' % build.dest),
             ('ERROR', 'No "Initiating shutdown" found in logs, maybe because of cpu limit.')
         ])
         # no loaded
+        build = self.Build.create({
+            'params_id': self.base_params.id,
+        })
         logs = []
         file_content = """
 Loading stuff
 """
         with patch('builtins.open', mock_open(read_data=file_content)):
-            result = config_step._make_results(build)
-        self.assertEqual(result, {'job_end': '1970-01-01 02:00:00', 'local_result': 'ko'})
+            config_step._make_results(build)
+        self.assertEqual(str(build.job_end), '1970-01-01 02:00:00')
+        self.assertEqual(build.local_result, 'ko')
         self.assertEqual(logs, [
             ('INFO', 'Getting results for build %s' % build.dest),
             ('ERROR', 'Modules loaded not found in logs')
         ])
 
         # traceback
+        build = self.Build.create({
+            'params_id': self.base_params.id,
+        })
         logs = []
         file_content = """
 Loading stuff
@@ -607,14 +620,18 @@ File "x.py", line a, in test_
 Initiating shutdown
 """
         with patch('builtins.open', mock_open(read_data=file_content)):
-            result = config_step._make_results(build)
-        self.assertEqual(result, {'job_end': '1970-01-01 02:00:00', 'local_result': 'ko'})
+            config_step._make_results(build)
+        self.assertEqual(str(build.job_end), '1970-01-01 02:00:00')
+        self.assertEqual(build.local_result, 'ko')
         self.assertEqual(logs, [
             ('INFO', 'Getting results for build %s' % build.dest),
             ('ERROR', 'Error or traceback found in logs')
         ])
 
         # warning in logs
+        build = self.Build.create({
+            'params_id': self.base_params.id,
+        })
         logs = []
         file_content = """
 Loading stuff
@@ -624,8 +641,9 @@ Some post install stuff
 Initiating shutdown
 """
         with patch('builtins.open', mock_open(read_data=file_content)):
-            result = config_step._make_results(build)
-        self.assertEqual(result, {'job_end': '1970-01-01 02:00:00', 'local_result': 'warn'})
+            config_step._make_results(build)
+        self.assertEqual(str(build.job_end), '1970-01-01 02:00:00')
+        self.assertEqual(build.local_result, 'warn')
         self.assertEqual(logs, [
             ('INFO', 'Getting results for build %s' % build.dest),
             ('WARNING', 'Warning found in logs')
@@ -634,15 +652,18 @@ Initiating shutdown
         # no log file
         logs = []
         self.patchers['isfile'].return_value = False
-        result = config_step._make_results(build)
+        config_step._make_results(build)
 
-        self.assertEqual(result, {'local_result': 'ko'})
+        self.assertEqual(build.local_result, 'ko')
         self.assertEqual(logs, [
             ('INFO', 'Getting results for build %s' % build.dest),
             ('ERROR', 'Log file not found at the end of test job')
         ])
 
         # no error but build was already in warn
+        build = self.Build.create({
+            'params_id': self.base_params.id,
+        })
         logs = []
         file_content = """
 Loading stuff
@@ -653,11 +674,12 @@ Initiating shutdown
         self.patchers['isfile'].return_value = True
         build.local_result = 'warn'
         with patch('builtins.open', mock_open(read_data=file_content)):
-            result = config_step._make_results(build)
+            config_step._make_results(build)
         self.assertEqual(logs, [
             ('INFO', 'Getting results for build %s' % build.dest)
         ])
-        self.assertEqual(result, {'job_end': '1970-01-01 02:00:00', 'local_result': 'warn'})
+        self.assertEqual(str(build.job_end), '1970-01-01 02:00:00')
+        self.assertEqual(build.local_result, 'warn')
 
     @patch('odoo.addons.runbot.models.build_config.ConfigStep._make_tests_results')
     def test_make_python_result(self, mock_make_tests_results):
@@ -672,18 +694,18 @@ Initiating shutdown
         })
         build.local_state = 'testing'
         self.patchers['isfile'].return_value = False
-        result = config_step._make_results(build)
-        self.assertEqual(result, {'local_result': 'ok'})
+        config_step._make_results(build)
+        self.assertEqual(build.local_result, 'ok')
 
         # invalid result code (no return_value set)
         config_step.python_result_code = """a = 2*5\nr = {'a': 'ok'}\nreturn_value = 'ko'"""
         with self.assertRaises(RunbotException):
-            result = config_step._make_results(build)
+            config_step._make_results(build)
 
         # no result defined
         config_step.python_result_code = ""
-        mock_make_tests_results.return_value = {'local_result': 'warning'}
-        result = config_step._make_results(build)
-        self.assertEqual(result, {'local_result': 'warning'})
+        mock_make_tests_results.return_value = {'local_result': 'warn'}
+        config_step._make_results(build)
+        self.assertEqual(build.local_result, 'warn')
 
 # TODO add generic test to copy_paste _run_* in a python step
diff --git a/runbot/tests/test_build_error.py b/runbot/tests/test_build_error.py
index c7cca257..0d616ab6 100644
--- a/runbot/tests/test_build_error.py
+++ b/runbot/tests/test_build_error.py
@@ -33,8 +33,10 @@ class TestBuildError(RunbotCase):
 
     def test_build_scan(self):
         IrLog = self.env['ir.logging']
-        ko_build = self.create_test_build({'local_result': 'ko'})
-        ok_build = self.create_test_build({'local_result': 'ok'})
+        ko_build = self.create_test_build({'local_result': 'ok', 'local_state': 'testing'})
+        ok_build = self.create_test_build({'local_result': 'ok', 'local_state': 'running'})
+
+
 
         error_team = self.BuildErrorTeam.create({
             'name': 'test-error-team',
@@ -56,6 +58,10 @@ class TestBuildError(RunbotCase):
         IrLog.create(log)
         log.update({'build_id': ok_build.id})
         IrLog.create(log)
+
+        self.assertEqual(ko_build.local_result, 'ko', 'Testing build should have gone ko after error log')
+        self.assertEqual(ok_build.local_result, 'ok', 'Running build should not have gone ko after error log')
+
         ko_build._parse_logs()
         ok_build._parse_logs()
         build_error = self.BuildError.search([('build_ids', 'in', [ko_build.id])])
diff --git a/runbot/tests/test_host.py b/runbot/tests/test_host.py
index fb839910..2aeb24f5 100644
--- a/runbot/tests/test_host.py
+++ b/runbot/tests/test_host.py
@@ -65,7 +65,7 @@ class TestHost(RunbotCase):
         # check that local logs are inserted in leader ir.logging
         logs = fetch_local_logs_return_value(build_dest=build.dest)
         self.start_patcher('fetch_local_logs', 'odoo.addons.runbot.models.host.Host._fetch_local_logs', logs)
-        self.test_host.process_logs()
+        self.test_host._process_logs()
         self.patchers['host_local_pg_cursor'].assert_called()
         self.assertEqual(
             self.env['ir.logging'].search_count([
@@ -78,7 +78,7 @@ class TestHost(RunbotCase):
         # check that a warn log sets the build in warning
         logs = fetch_local_logs_return_value(nb_logs=1, build_dest=build.dest, level='WARNING')
         self.patchers['fetch_local_logs'].return_value = logs
-        self.test_host.process_logs()
+        self.test_host._process_logs()
         self.patchers['host_local_pg_cursor'].assert_called()
         self.assertEqual(
             self.env['ir.logging'].search_count([
@@ -88,12 +88,12 @@ class TestHost(RunbotCase):
             ]),
             1,
         )
-        self.assertEqual(build.triggered_result, 'warn', 'A warning log should sets the build in warn')
+        self.assertEqual(build.local_result, 'warn', 'A warning log should sets the build in warn')
 
         # now check that error logs sets the build in ko
         logs = fetch_local_logs_return_value(nb_logs=1, build_dest=build.dest, level='ERROR')
         self.patchers['fetch_local_logs'].return_value = logs
-        self.test_host.process_logs()
+        self.test_host._process_logs()
         self.patchers['host_local_pg_cursor'].assert_called()
         self.assertEqual(
             self.env['ir.logging'].search_count([
@@ -103,11 +103,11 @@ class TestHost(RunbotCase):
             ]),
             1,
         )
-        self.assertEqual(build.triggered_result, 'ko', 'An error log should sets the build in ko')
+        self.assertEqual(build.local_result, 'ko', 'An error log should sets the build in ko')
 
         build.log_counter = 10
         # Test log limit
         logs = fetch_local_logs_return_value(nb_logs=11, message='test log limit', build_dest=build.dest)
         self.patchers['fetch_local_logs'].return_value = logs
-        self.test_host.process_logs()
+        self.test_host._process_logs()
         self.patchers['host_local_pg_cursor'].assert_called()
diff --git a/runbot/tests/test_schedule.py b/runbot/tests/test_schedule.py
index ba9dcddb..0e47f6d7 100644
--- a/runbot/tests/test_schedule.py
+++ b/runbot/tests/test_schedule.py
@@ -23,6 +23,7 @@ class TestSchedule(RunbotCase):
 
         build = self.Build.create({
             'local_state': 'testing',
+            'global_state': 'testing',
             'port': '1234',
             'host': host.name,
             'job_start': datetime.datetime.now(),
@@ -33,9 +34,10 @@ class TestSchedule(RunbotCase):
         self.assertEqual(build.local_state, 'testing')
         build._schedule()  # too fast, docker not started
         self.assertEqual(build.local_state, 'testing')
+        self.assertEqual(build.local_result, 'ok')
 
         self.start_patcher('fetch_local_logs', 'odoo.addons.runbot.models.host.Host._fetch_local_logs', [])  # the local logs have to be empty
         build.write({'job_start': datetime.datetime.now() - datetime.timedelta(seconds=70)})  # docker never started
         build._schedule()
         self.assertEqual(build.local_state, 'done')
-        self.assertEqual(build.local_result, 'ok')
+        self.assertEqual(build.local_result, 'ko')
diff --git a/runbot/tests/test_upgrade.py b/runbot/tests/test_upgrade.py
index e8c9ce21..09044bf3 100644
--- a/runbot/tests/test_upgrade.py
+++ b/runbot/tests/test_upgrade.py
@@ -1,6 +1,6 @@
 import getpass
 import logging
-import getpass
+from unittest.mock import patch, mock_open
 from odoo.exceptions import UserError
 from odoo.tools import mute_logger
 from .common import RunbotCase
@@ -270,9 +270,9 @@ class TestUpgradeFlow(RunbotCase):
         batch = self.master_bundle._force()
         batch._prepare()
         upgrade_current_build = batch.slot_ids.filtered(lambda slot: slot.trigger_id == self.trigger_upgrade_server).build_id
-        host = self.env['runbot.host']._get_current()
-        upgrade_current_build.host = host.name
-        upgrade_current_build._init_pendings(host)
+        #host = self.env['runbot.host']._get_current()
+        #upgrade_current_build.host = host.name
+        upgrade_current_build._schedule()
         self.start_patcher('fetch_local_logs', 'odoo.addons.runbot.models.host.Host._fetch_local_logs', [])  # the local logs have to be empty
         upgrade_current_build._schedule()
         self.assertEqual(upgrade_current_build.local_state, 'done')
@@ -296,9 +296,8 @@ class TestUpgradeFlow(RunbotCase):
 
         # upgrade repos tests
         upgrade_build = batch.slot_ids.filtered(lambda slot: slot.trigger_id == self.trigger_upgrade).build_id
-        host = self.env['runbot.host']._get_current()
-        upgrade_build.host = host.name
-        upgrade_build._init_pendings(host)
+        #upgrade_build.host = host.name
+        upgrade_build._schedule()
         upgrade_build._schedule()
         self.assertEqual(upgrade_build.local_state, 'done')
         self.assertEqual(len(upgrade_build.children_ids), 2)
@@ -337,9 +336,8 @@ class TestUpgradeFlow(RunbotCase):
         batch = self.master_bundle._force(self.nightly_category.id)
         batch._prepare()
         upgrade_nightly = batch.slot_ids.filtered(lambda slot: slot.trigger_id == trigger_upgrade_addons_nightly).build_id
-        host = self.env['runbot.host']._get_current()
-        upgrade_nightly.host = host.name
-        upgrade_nightly._init_pendings(host)
+        #upgrade_nightly.host = host.name
+        upgrade_nightly._schedule()
         upgrade_nightly._schedule()
         to_version_builds = upgrade_nightly.children_ids
         self.assertEqual(upgrade_nightly.local_state, 'done')
@@ -352,10 +350,15 @@ class TestUpgradeFlow(RunbotCase):
             to_version_builds.mapped('params_id.upgrade_from_build_id.params_id.version_id.name'),
             []
         )
-        to_version_builds.host = host.name
-        to_version_builds._init_pendings(host)
-        to_version_builds._schedule()
-        self.assertEqual(to_version_builds.mapped('local_state'), ['done']*4)
+        #to_version_builds.host = host.name
+        for build in to_version_builds:
+            build._schedule()  # starts builds
+            self.assertEqual(build.local_state, 'testing')
+            build._schedule()  # makes result and end build
+            self.assertEqual(build.local_state, 'done')
+
+        self.assertEqual(to_version_builds.mapped('global_state'), ['done', 'waiting', 'waiting', 'waiting'], 'One build have no child, other should wait for children')
+
         from_version_builds = to_version_builds.children_ids
         self.assertEqual(
             [
@@ -367,10 +370,15 @@ class TestUpgradeFlow(RunbotCase):
             ],
             ['11.0->12.0', 'saas-11.3->12.0', '12.0->13.0', 'saas-12.3->13.0', '13.0->master', 'saas-13.1->master', 'saas-13.2->master', 'saas-13.3->master']
         )
-        from_version_builds.host = host.name
-        from_version_builds._init_pendings(host)
-        from_version_builds._schedule()
-        self.assertEqual(from_version_builds.mapped('local_state'), ['done']*8)
+        #from_version_builds.host = host.name
+        for build in from_version_builds:
+            build._schedule()
+            self.assertEqual(build.local_state, 'testing')
+            build._schedule()
+            self.assertEqual(build.local_state, 'done')
+
+        self.assertEqual(from_version_builds.mapped('global_state'), ['waiting'] * 8)
+
         db_builds = from_version_builds.children_ids
         self.assertEqual(len(db_builds), 40)
 
@@ -405,61 +413,74 @@ class TestUpgradeFlow(RunbotCase):
             [b.params_id.dump_db.db_suffix for b in b133_master],
             ['account', 'l10n_be', 'l10n_ch', 'mail', 'stock']  # is this order ok?
         )
+        current_build = db_builds[0]
+        for current_build in db_builds:
+            self.start_patcher('docker_state', 'odoo.addons.runbot.models.build.docker_state', 'END')
 
-        first_build = db_builds[0]
+            suffix = current_build.params_id.dump_db.db_suffix
+            source_dest = current_build.params_id.dump_db.build_id.dest
 
-        self.start_patcher('docker_state', 'odoo.addons.runbot.models.build.docker_state', 'END')
+            def docker_run_restore(cmd, *args, **kwargs):
+                dump_url = f'http://host.runbot.com/runbot/static/build/{source_dest}/logs/{source_dest}-{suffix}.zip'
+                zip_name = f'{source_dest}-{suffix}.zip'
+                db_name = f'{current_build.dest}-{suffix}'
+                self.assertEqual(
+                    str(cmd).split(' && '),
+                    [
+                        'mkdir /data/build/restore',
+                        'cd /data/build/restore',
+                        f'wget {dump_url}',
+                        f'unzip -q {zip_name}',
+                        'echo "### restoring filestore"',
+                        f'mkdir -p /data/build/datadir/filestore/{db_name}',
+                        f'mv filestore/* /data/build/datadir/filestore/{db_name}',
+                        'echo "### restoring db"',
+                        f'psql -q {db_name} < dump.sql',
+                        'cd /data/build',
+                        'echo "### cleaning"',
+                        'rm -r restore',
+                        'echo "### listing modules"',
+                        f'psql {db_name} -c "select name from ir_module_module where state = \'installed\'" -t -A > /data/build/logs/restore_modules_installed.txt',
+                        'echo "### restore" "successful"'
+                    ]
+                )
+            self.patchers['docker_run'].side_effect = docker_run_restore
+            #current_build.host = host.name
+            current_build._schedule()()
+            self.patchers['docker_run'].assert_called()
 
-        def docker_run_restore(cmd, *args, **kwargs):
-            source_dest = first_build.params_id.dump_db.build_id.dest
-            dump_url='http://host.runbot.com/runbot/static/build/%s/logs/%s-account.zip' % (source_dest, source_dest)
-            zip_name='%s-account.zip' % source_dest
-            db_name='%s-master-account' % str(first_build.id).zfill(5)
-            self.assertEqual(
-                str(cmd).split(' && '),
-                [
-                    'mkdir /data/build/restore',
-                    'cd /data/build/restore',
-                    f'wget {dump_url}',
-                    f'unzip -q {zip_name}',
-                    'echo "### restoring filestore"',
-                    f'mkdir -p /data/build/datadir/filestore/{db_name}',
-                    f'mv filestore/* /data/build/datadir/filestore/{db_name}',
-                    'echo "### restoring db"',
-                    f'psql -q {db_name} < dump.sql',
-                    'cd /data/build',
-                    'echo "### cleaning"',
-                    'rm -r restore',
-                    'echo "### listing modules"',
-                    f'psql {db_name} -c "select name from ir_module_module where state = \'installed\'" -t -A > /data/build/logs/restore_modules_installed.txt',
-                    'echo "### restore" "successful"'
-                ]
-            )
-        self.patchers['docker_run'].side_effect = docker_run_restore
-        first_build.host = host.name
-        first_build._init_pendings(host)
-        self.patchers['docker_run'].assert_called()
+            def docker_run_upgrade(cmd, *args, ro_volumes=False, **kwargs):
+                user = getpass.getuser()
+                self.assertTrue(ro_volumes.pop(f'/home/{user}/.odoorc').startswith('/tmp/runbot_test/static/build/'))
+                self.assertEqual(
+                    list(ro_volumes.keys()), [
+                        '/data/build/addons',
+                        '/data/build/server',
+                        '/data/build/upgrade',
+                    ],
+                    "other commit should have been added automaticaly"
+                )
+                self.assertEqual(
+                    str(cmd),
+                    'python3 server/server.py {addons_path} --no-xmlrpcs --no-netrpc -u all -d {db_name} --stop-after-init --max-cron-threads=0'.format(
+                        addons_path='--addons-path addons,server/addons,server/core/addons',
+                        db_name=f'{current_build.dest}-{suffix}')
+                )
+            self.patchers['docker_run'].side_effect = docker_run_upgrade
+            current_build._schedule()()
 
-        def docker_run_upgrade(cmd, *args, ro_volumes=False, **kwargs):
-            user = getpass.getuser()
-            self.assertTrue(ro_volumes.pop(f'/home/{user}/.odoorc').startswith('/tmp/runbot_test/static/build/'))
-            self.assertEqual(
-                ro_volumes, {
-                    '/data/build/addons': '/tmp/runbot_test/static/sources/addons/addons120',
-                    '/data/build/server': '/tmp/runbot_test/static/sources/server/server120',
-                    '/data/build/upgrade': '/tmp/runbot_test/static/sources/upgrade/123abc789',
-                },
-                "other commit should have been added automaticaly"
-            )
-            self.assertEqual(
-                str(cmd),
-                'python3 server/server.py {addons_path} --no-xmlrpcs --no-netrpc -u all -d {db_name} --stop-after-init --max-cron-threads=0'.format(
-                    addons_path='--addons-path addons,server/addons,server/core/addons',
-                    db_name='%s-master-account' % str(first_build.id).zfill(5))
-            )
-        self.patchers['docker_run'].side_effect = docker_run_upgrade
-        first_build._schedule()
-        self.assertEqual(self.patchers['docker_run'].call_count, 2)
+            with patch('builtins.open', mock_open(read_data='')):
+                current_build._schedule()
+            self.assertEqual(current_build.local_state, 'done')
+
+            self.assertEqual(current_build.global_state, 'done')
+            # self.assertEqual(current_build.global_result, 'ok')
+
+        self.assertEqual(self.patchers['docker_run'].call_count, 80)
+
+        self.assertEqual(from_version_builds.mapped('global_state'), ['done'] * 8)
+
+        self.assertEqual(to_version_builds.mapped('global_state'), ['done'] * 4)
 
         # test_build_references
         batch = self.master_bundle._force()
@@ -520,12 +541,12 @@ class TestUpgradeFlow(RunbotCase):
         batch13 = bundle_13._force()
         batch13._prepare()
         upgrade_complement_build_13 = batch13.slot_ids.filtered(lambda slot: slot.trigger_id == trigger_upgrade_complement).build_id
-        upgrade_complement_build_13.host = host.name
+        # upgrade_complement_build_13.host = host.name
         self.assertEqual(upgrade_complement_build_13.params_id.config_id, config_upgrade_complement)
         for db in ['base', 'all', 'no-demo-all']:
             upgrade_complement_build_13.database_ids = [(0, 0, {'name': '%s-%s' % (upgrade_complement_build_13.dest, db)})]
 
-        upgrade_complement_build_13._init_pendings(host)
+        upgrade_complement_build_13._schedule()
 
         self.assertEqual(len(upgrade_complement_build_13.children_ids), 5)
         master_child = upgrade_complement_build_13.children_ids[0]
@@ -534,6 +555,7 @@ class TestUpgradeFlow(RunbotCase):
         self.assertEqual(master_child.params_id.config_id, self.test_upgrade_config)
         self.assertEqual(master_child.params_id.upgrade_to_build_id.params_id.version_id.name, 'master')
 
+
 class TestUpgrade(RunbotCase):
 
     def test_exceptions_in_env(self):
diff --git a/runbot/views/build_views.xml b/runbot/views/build_views.xml
index 41fe358c..af2bdb25 100644
--- a/runbot/views/build_views.xml
+++ b/runbot/views/build_views.xml
@@ -68,7 +68,6 @@
                         <field name="requested_action" groups="base.group_no_one"/>
                         <field name="local_result"/>
                         <field name="global_result"/>
-                        <field name="triggered_result" groups="base.group_no_one"/>
                         <field name="host"/>
                         <field name="host_id"/>
                         <field name="job_start" groups="base.group_no_one"/>
diff --git a/runbot/views/config_views.xml b/runbot/views/config_views.xml
index d10b644b..20accff7 100644
--- a/runbot/views/config_views.xml
+++ b/runbot/views/config_views.xml
@@ -61,7 +61,6 @@
                         <field name="python_code" widget="ace" options="{'mode': 'python'}"/>
                         <field name="python_result_code" widget="ace" options="{'mode': 'python'}"/>
                         <field name="running_job"/>
-                        <field name="ignore_triggered_result"/>
                     </group>
                     <group string="Test settings" attrs="{'invisible': [('job_type', 'not in', ('python', 'install_odoo'))]}">
                         <field name="create_db" groups="base.group_no_one"/>