Hi Ihor,

thanks again for you input.  I've reworked the proposal once more.

Ihor Radchenko <[email protected]> writes:

>> Would it still compose as a macro?  E.g., could I call one block
>> assigning its variables using the result of two others?
>
> Yes.

Good to know, yet I'd prefer to keep it a function and variable names
conventional string arguments which might be crafted by some expression
as well.

>> (org-babel-call "foo" positional1 positional2 :dir "/home/daniel")
>
> What if you need to pass :dir or other keyword as value?

You're right, thanks for pointing that out.  I think, the practical way
is to provide such a variable explicitly with `:var` in such a case,
instead of passing it positionally.  This new version handles a keyword
value cleanly and addresses your other comments.

>> (org-babel-call "bar" :var "var1" lisp-variable)  ;; (1)
>
> This may work.
>
>> (org-babel-call "xyz" :var "var2=ref[3]")         ;; (2)
>
> Looks like defeating the purpose of pluggable ELisp.

I simply removed variant (2) entirely.  A `:var` keyword now always
takes exactly two arguments, a NAME string and the VALUE, of which the
VALUE is used as-is, so a keyword is no longer a problem there either:

    (org-babel-call "foo" :var "a" :some-keyword)

If someone wants to define a variable using a resolved reference,
e.g. to read from a table, they can still use `org-babel-ref-resolve`
explicitly (positionally or named).  I mentioned that in the docstring.

    (org-babel-call "foo" :var "a" (org-babel-ref-resolve "tbl[1,1]"))

Attached find v4 of the patch.

This patch revision also includes some tests, which the ealier versions
did not.

Regarding your comments on the complementary function
`org-babel-result`:

> Why not just :var hello=hello[:cache yes]() +
> (setq org-babel-update-intermediate 'cache)?

I checked this and I don't think it's a substitute for my proposed
function.  The idea of `org-babel-result` is to have something that
obviously and reliably never modifies or re-executes the producing block
(regardless of any settings).

Let's say, the referenced block took an hour to run (I happen to have
such queries) or even is a one-shot thing whose result isn't
reproducible at all.  Now with the result already in the buffer, I want
to consume the result in another block.

With the proposed `org-babel-result` I can do that in an elisp body or
in a variable assignment in the header.

I'm looking forward to turn in `org-babel-result` once we have settled
on the syntax for `org-babel-call`.

Kind regards,
Daniel

>From 9aeb42a86e949e17a0be4663b75474bef7230677 Mon Sep 17 00:00:00 2001
From: Daniel Bausch <[email protected]>
Date: Mon, 6 Jul 2026 14:53:38 +0200
Subject: [PATCH] ob-core: Add org-babel-call

* lisp/ob-core.el (org-babel-call): New function.  Call a named
source block from Lisp following the argument model of a "#+call:"
line: positional arguments bind the block's declared variables in
Babel's declaration order as real Lisp objects; the `:var' keyword
assigns one variable by name and always takes exactly two
arguments, NAME and VALUE, with VALUE bound exactly as given, of
any type -- to use the reference syntax instead (table indexing,
a named block call), call `org-babel-ref-resolve' and pass its
result as VALUE; any other keyword is a header argument merged
into the block's execution, each taking exactly one Lisp value.
Built on `org-babel-execute-src-block' with cons-valued `:var'
parameters so values are forwarded as objects rather than
serialized into a string, and located with
`org-babel-find-named-block' to avoid the buffer side effects of
`org-babel-goto-named-src-block'.  During export the block is
resolved in `org-babel-exp-reference-buffer', like Babel reference
resolution, so calls executed at export time find blocks that are
themselves not exported.  Argument parsing errors clearly if a
non-keyword is found where a keyword is expected (most likely a
missing `:var' value) instead of silently misparsing the rest of
the argument list.
* etc/ORG-NEWS (New functions and changes in function arguments):
Announce it.
* testing/lisp/test-ob.el (test-ob-core/org-babel-call-positional)
(test-ob-core/org-babel-call-var-by-value)
(test-ob-core/org-babel-call-var-keyword-value)
(test-ob-core/org-babel-call-header-arg)
(test-ob-core/org-babel-call-results-locked)
(test-ob-core/org-babel-call-too-many-positional)
(test-ob-core/org-babel-call-missing-block)
(test-ob-core/org-babel-call-var-requires-name-and-value)
(test-ob-core/org-babel-call-var-name-must-be-string)
(test-ob-core/org-babel-call-reference-resolution)
(test-ob-core/org-babel-call-composes): New tests.
---
 etc/ORG-NEWS            | 22 ++++++++++
 lisp/ob-core.el         | 71 ++++++++++++++++++++++++++++++
 testing/lisp/test-ob.el | 95 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 188 insertions(+)

diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 9263bfeb0..2f687e1da 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -242,6 +242,28 @@ information.
 Given the completed and total number of tasks, format the percent
 cookie =[N%]=.
 
+*** New function ~org-babel-call~
+
+Execute a named source block and return its result with its type
+intact:
+
+#+begin_src emacs-lisp
+(org-babel-call "summary" 2026 :var "rows" data :dir "/srv")
+#+end_src
+
+Arguments before the first keyword bind the block's declared
+variables positionally, in declaration order.  The keyword ~:var~
+binds one variable by name and always takes exactly two arguments,
+=:var "name" VALUE=; NAME is a string, so it can be computed, and
+VALUE is any Lisp object, bound as-is.  Any other keyword is a
+header argument merged into the block's execution, e.g. ~:dir~ or
+~:cmdline~ — each takes exactly one Lisp value, so a multi-word
+header-line value such as ~:cmdline~'s is one Lisp string
+(~:cmdline "-O2 -Wall"~).  Unlike a =#+call:= line, there is no
+reference syntax: for table indexing or a block call, use
+~org-babel-ref-resolve~ and pass its result as VALUE.  ~:results~
+cannot be overridden.
+
 ** Removed or renamed functions and variables
 
 *** ~org-babel-remote-temporary-directory~ is now obsolete
diff --git a/lisp/ob-core.el b/lisp/ob-core.el
index 7f283012c..374269325 100644
--- a/lisp/ob-core.el
+++ b/lisp/ob-core.el
@@ -982,6 +982,77 @@ guess will be made."
 	    (run-hooks 'org-babel-after-execute-hook)
 	    result)))))))
 
+;;;###autoload
+(defun org-babel-call (name &rest args)
+  "Execute the source block named NAME, binding its variables to the
+values given, and return the result with its type intact.
+
+Arguments before the first keyword bind the block's declared
+variables positionally, in declaration order.  The keyword `:var'
+binds one variable by name and always takes two arguments, NAME
+and VALUE.  NAME is a string, an ordinary expression rather than a
+literal, so it can be computed; VALUE is any Lisp object, bound
+as-is.  Any other keyword is a header argument merged into the
+block's execution, e.g. `:dir' or `:cmdline'.  Each keyword takes
+exactly one Lisp value, so a value that looks like several words
+in header-line syntax, such as `:cmdline' \"-O2 -Wall\", is a
+single Lisp string here, not two arguments.
+
+Unlike a \"#+call:\" line, there is no reference syntax: for table
+indexing or a block call, use `org-babel-ref-resolve' and pass its
+result as VALUE.  `:results' cannot be overridden.
+
+\(fn NAME [POSITIONAL...] [:var NAME VALUE]... [KEYWORD VALUE]...)"
+  ;; Resolve against the pristine buffer during export, like other
+  ;; Babel references, so a call finds blocks not themselves exported.
+  (with-current-buffer (or org-babel-exp-reference-buffer (current-buffer))
+    (let* ((location (or (org-babel-find-named-block name)
+                         (error "No source block named `%s'" name)))
+           (info (org-with-point-at location
+                   (org-babel-get-src-block-info 'no-eval)))
+           (declared (org-babel--get-vars (nth 2 info)))
+           (params nil))
+      ;; Positional arguments bind declared variables in order.
+      (while (and args (not (keywordp (car args))))
+        (let ((decl (pop declared)))
+          (unless decl
+            (error "Too many positional arguments calling block `%s'" name))
+          (push (cons :var
+                      (cons (cond ((consp decl) (car decl))
+                                  ((and (stringp decl)
+                                        (string-match
+                                         "^\\([^= \f\t\n\r\v]+\\)[ \t]*=" decl))
+                                   (intern (match-string 1 decl)))
+                                  (t (error "Cannot bind variable `%S'" decl)))
+                            (pop args)))
+                params)))
+      ;; Keyword arguments: `:var' bindings and header-argument overrides.
+      (while args
+        (let ((key (pop args)))
+          (unless (keywordp key)
+            (error "Expected a keyword, got `%S' calling block `%s' -- missing a `:var' value?"
+                   key name))
+          (pcase key
+            (:var
+             (unless (cdr args)
+               (error "`:var' requires a name and a value calling block `%s'" name))
+             (let ((var-name (pop args))
+                   (value (pop args)))
+               (unless (stringp var-name)
+                 (error "`:var' name must be a string, not `%S', calling block `%s'"
+                        var-name name))
+               (push (cons :var (cons (intern var-name) value)) params)))
+            (:results
+             (error "`:results' cannot be set as a header argument"))
+            (_
+             (unless args
+               (error "Missing value for `%s' calling block `%s'" key name))
+             (push (cons key (pop args)) params)))))
+      ;; Forced silent so nothing is inserted and the result composes
+      ;; as an ordinary Lisp value.
+      (org-babel-execute-src-block
+       nil info (append (nreverse params) '((:results . "silent")))))))
+
 (defun org-babel-expand-body:generic (body params &optional var-lines)
   "Expand BODY with PARAMS.
 Expand a block of code with org-babel according to its header
diff --git a/testing/lisp/test-ob.el b/testing/lisp/test-ob.el
index 3644a511b..7572ca57c 100644
--- a/testing/lisp/test-ob.el
+++ b/testing/lisp/test-ob.el
@@ -2912,6 +2912,101 @@ A
           (should (string= (org-element-property :value eap)
                            (concat region "\n"))))))))
 
+(ert-deftest test-ob-core/org-babel-call-positional ()
+  "Positional arguments bind declared variables in order, as real
+Lisp objects."
+  (should
+   (equal 30
+          (org-test-with-temp-text
+              "#+name: add\n#+begin_src emacs-lisp :var a=1 :var b=2\n(+ a b)\n#+end_src\n"
+            (org-babel-call "add" 10 20)))))
+
+(ert-deftest test-ob-core/org-babel-call-var-by-value ()
+  "`:var NAME VALUE' binds VALUE exactly as given."
+  (should
+   (equal 30
+          (org-test-with-temp-text
+              "#+name: add\n#+begin_src emacs-lisp :var a=1 :var b=2\n(+ a b)\n#+end_src\n"
+            (org-babel-call "add" :var "a" 10 :var "b" 20)))))
+
+(ert-deftest test-ob-core/org-babel-call-var-keyword-value ()
+  "A `:var' VALUE that is itself a keyword is bound literally, not
+mistaken for the start of the next keyword argument."
+  (should
+   (eq :foo
+       (org-test-with-temp-text
+           "#+name: echo\n#+begin_src emacs-lisp :var a=1\na\n#+end_src\n"
+         (org-babel-call "echo" :var "a" :foo)))))
+
+(ert-deftest test-ob-core/org-babel-call-header-arg ()
+  "Any keyword other than `:var' is a header argument merged into
+the block's execution."
+  (should
+   (equal "/tmp/"
+          (org-test-with-temp-text
+              "#+name: showdir\n#+begin_src emacs-lisp\ndefault-directory\n#+end_src\n"
+            (org-babel-call "showdir" :dir "/tmp/")))))
+
+(ert-deftest test-ob-core/org-babel-call-results-locked ()
+  "`:results' cannot be overridden."
+  (should-error
+   (org-test-with-temp-text
+       "#+name: foo\n#+begin_src emacs-lisp\n1\n#+end_src\n"
+     (org-babel-call "foo" :results "value"))))
+
+(ert-deftest test-ob-core/org-babel-call-too-many-positional ()
+  "Supplying more positional arguments than declared variables is
+an error."
+  (should-error
+   (org-test-with-temp-text
+       "#+name: one-var\n#+begin_src emacs-lisp :var a=1\na\n#+end_src\n"
+     (org-babel-call "one-var" 10 20))))
+
+(ert-deftest test-ob-core/org-babel-call-missing-block ()
+  "Calling a name with no matching source block is an error."
+  (should-error
+   (org-test-with-temp-text "text, no blocks\n"
+     (org-babel-call "nope"))))
+
+(ert-deftest test-ob-core/org-babel-call-var-requires-name-and-value ()
+  "`:var' with a missing value is a clear error, not a silent
+misparse of whatever keyword follows."
+  (should-error
+   (org-test-with-temp-text
+       "#+name: echo\n#+begin_src emacs-lisp :var a=1\na\n#+end_src\n"
+     (org-babel-call "echo" :var "a" :var "b" 1))))
+
+(ert-deftest test-ob-core/org-babel-call-var-name-must-be-string ()
+  "`:var' NAME must be a string."
+  (should-error
+   (org-test-with-temp-text
+       "#+name: echo\n#+begin_src emacs-lisp :var a=1\na\n#+end_src\n"
+     (org-babel-call "echo" :var 'a 1))))
+
+(ert-deftest test-ob-core/org-babel-call-reference-resolution ()
+  "Org's reference syntax (table indexing, etc.) is available by
+calling `org-babel-ref-resolve' directly and passing its result as
+VALUE -- `org-babel-call' has no dedicated syntax of its own for
+this."
+  (should
+   (equal 2
+          (org-test-with-temp-text
+              (concat "#+name: tbl\n#+begin_src emacs-lisp\n'((1 2) (3 4))\n#+end_src\n\n"
+                      "#+name: showcell\n#+begin_src emacs-lisp :var cell=1\ncell\n#+end_src\n")
+            (org-babel-call "showcell" :var "cell" (org-babel-ref-resolve "tbl[0,1]"))))))
+
+(ert-deftest test-ob-core/org-babel-call-composes ()
+  "Positional and `:var' values are ordinary Lisp expressions,
+evaluated the normal way -- variables and nested `org-babel-call'
+results compose with no special quoting convention."
+  (should
+   (equal 107
+          (org-test-with-temp-text
+              (concat "#+name: seven\n#+begin_src emacs-lisp\n7\n#+end_src\n\n"
+                      "#+name: add\n#+begin_src emacs-lisp :var a=1 :var b=2\n(+ a b)\n#+end_src\n")
+            (let ((my-var 100))
+              (org-babel-call "add" my-var (org-babel-call "seven")))))))
+
 (provide 'test-ob)
 
 ;;; test-ob.el ends here
-- 
2.55.0

Reply via email to