bpy.ops.object.bake('INVOKE_DEFAULT', type='DIFFUSE')

Does not work because the bake uses 'INVOKE_DEFAULT' and thus save_image will be executed directly.

How to run save_image after the baking has finished?

  • $\begingroup$ Don't invoke default? If you are after user input could write an operator or macro to invoke and feed arguments into bake op. $\endgroup$
    – batFINGER
    Mar 21, 2020 at 9:48
  • $\begingroup$ But this freezes the UI and no progress bar is shown $\endgroup$ Mar 21, 2020 at 10:00
  • $\begingroup$ @RobertGützkow sounds good, but I can't find a handler for "bake_post" or something similar $\endgroup$ Mar 21, 2020 at 11:00
  • $\begingroup$ @RobertGützkow unfortunately, neither render_post nor render_complete is triggered $\endgroup$ Mar 21, 2020 at 11:31
  • 1
    $\begingroup$ The options to save the baked image externally doesn't appear to work either. I'll put it on my to do list. $\endgroup$
    – Robert Gützkow
    Mar 21, 2020 at 12:21

2 Answers 2


Although the question is old, can help more developers.
You can create a modal operator and check the state of the 'is_dirty' property of the image.

import bpy

class ModalTimerOperator(bpy.types.Operator):
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"
    _timer = None
    _img = None

    def modal(self, context, event):
        if event.type in {'RIGHTMOUSE', 'ESC'}:
            return {'CANCELLED'}

        if event.type == 'TIMER':
            if self._img.is_dirty: # <--- Wait until the image is marked as dirty
                return {'FINISHED'}

        return {'PASS_THROUGH'}

    def execute(self, context: bpy.context):
        if context.scene.render.engine != 'CYCLES':
            context.scene.render.engine = 'CYCLES'
        if not context.active_object.select_get() or not context.active_object.type == 'MESH':
            self.report({'WARNING'}, "No valid selected objects")
            return {'FINISHED'}

        obj = context.active_object
        mats = obj.data.materials
        mats_len = len(mats)

        if mats_len == 0 or not mats[0]:
            mat_new = bpy.data.materials.new('BakeMaterial')
            if mats_len == 0:
                mats[0] = mat_new

        mat = mats[0]
        mat.use_nodes = True
        nodes = mat.node_tree.nodes
        tex_node = nodes.new('ShaderNodeTexImage')
        tex_node.name = 'BakeNode'
        tex_node.select = True
        nodes.active = tex_node
        self._img = bpy.data.images.new('BakeResult', 1024, 1024)
        tex_node.image = self._img

        self.report({'INFO'}, "Execute")
        result = bpy.ops.object.bake('INVOKE_DEFAULT', type='DIFFUSE')
        if result != {'RUNNING_MODAL'}: # <--- Important to check the result
            self.report({'WARNING'}, "Failed to start baking")
            return {'FINISHED'}

        wm = context.window_manager
        self._timer = wm.event_timer_add(0.5, window=context.window)
        return {'RUNNING_MODAL'}

    def cancel(self, context):
        self.report({'INFO'}, "Baking map cancelled")

    def finish(self, context):
        wm = context.window_manager
        self.report({'INFO'}, "Baking map completed")
        #save_image() # <--- Call the function after baking

def register():

def unregister():

if __name__ == "__main__":

Thank you. Great script. As a side note, once the image data is baked, is_dity returns true. If you want to bake that image over again, you need to pack the image once. In


, the value of is_dirty returns false. Now you can create a modus operandi that bakes images on top of each other.

When overlapping bakes, scene settings should also be reviewed.

bpy.context.scene.render.bake.use_clear = False

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .