Christian, Ihor, Thank you both — you were right. The #+NAME:/#+CAPTION: above #+RESULTS: approach works correctly with standard ob-python. The bug is in emacs-jupyter (ob-jupyter).
The root cause: jupyter-org--strip-properties strips :results from the old result element (so #+RESULTS: isn't re-emitted by org-element-interpret-data), but does not strip :name or :caption. When the old result is re-serialized inside a :RESULTS: drawer, those affiliated keywords are emitted again — duplicating the ones already preserved in the buffer by jupyter-org-delete-element. The fix is two lines in jupyter-org-client.el: (org-element-put-property element :name nil) (org-element-put-property element :caption nil) I've filed an issue and PR upstream: https://github.com/emacs-jupyter/jupyter/issues/610 https://github.com/emacs-jupyter/jupyter/pull/611 Tested with 16 tables in a production org document — captions survive re-execution cleanly now. Thanks for pointing me in the right direction. Peter
