diff --git a/docs/source/usage/project_integration.md b/docs/source/usage/project_integration.md
index 631a4123ba3e43c339ca46dec751e85926ed6893..491402cab0544f501ee77447e939b89c606c4987 100644
--- a/docs/source/usage/project_integration.md
+++ b/docs/source/usage/project_integration.md
@@ -122,6 +122,7 @@ pystencilssfg_generate_target_sources( <target>
     [FILE_EXTENSIONS <header-extension> <impl-extension>]
     [OUTPUT_MODE <standalone|inline|header-only>]
     [CONFIG_MODULE <path-to-config-module.py>]
+    [OUTPUT_DIRECTORY <output-directory>]
 )
 ```
 
@@ -138,8 +139,11 @@ The function takes the following options:
  - `CONFIG_MODULE`: Set the configuration module for all scripts registered with this call.
    If set, this overrides the value of `PystencilsSfg_CONFIG_MODULE`
    in the current scope (see [](#cmake_set_config_module))
+ - `OUTPUT_DIRECTORY`: Custom output directory for generated files. If `OUTPUT_DIRECTORY` is a relative path,
+   it will be interpreted relative to the current build directory.
 
-Any C++ header files generated by the above call can be included in any files belonging to `target` via:
+If `OUTPUT_DIRECTORY` is *not* specified, any C++ header files generated by the above call
+can be included in any files belonging to `target` via:
 
 ```C++
 #include "gen/<file1.hpp>"
@@ -147,12 +151,12 @@ Any C++ header files generated by the above call can be included in any files be
 /* ... */
 ```
 
-### Changing the Output Directory
+:::{attention}
+If you change the code generator output directory using the `OUTPUT_DIRECTORY` argument,
+you are yourself responsible for placing that directory--or any of its parents--on the
+include path of your target.
+:::
 
-The CMake function listed above will create a subdirectory `_gen/<target>` at the current point in
-the build tree (i.e. [`CMAKE_CURRENT_BINARY_DIR`](https://cmake.org/cmake/help/latest/variable/CMAKE_CURRENT_BINARY_DIR.html)).
-This directory is placed on the include path of `<target>`, and the generated files will be written to `_gen/<target>/gen/`
-such that they can be included with the `gen` prefix.
 
 (cmake_set_config_module)=
 ### Set a Configuration Module
diff --git a/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake b/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake
index 7f3b2da624c0e233ce815071b58d64b8024e6af2..17a7846e6a32761ae0490490acc645e65d376959 100644
--- a/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake
+++ b/src/pystencilssfg/cmake/modules/PystencilsSfg.cmake
@@ -19,17 +19,13 @@ if(NOT DEFINED CACHE{_Pystencils_Include_Dir})
     set(_Pystencils_Include_Dir ${_pystencils_includepath_result} CACHE PATH "")
 endif()
 
-function(_pssfg_add_gen_source target script)
+function(_pssfg_add_gen_source target script outputDirectory)
     set(options)
     set(oneValueArgs)
     set(multiValueArgs GENERATOR_ARGS USER_ARGS DEPENDS)
 
     cmake_parse_arguments(_pssfg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
 
-    set(generatedSourcesIncludeDir ${CMAKE_CURRENT_BINARY_DIR}/_gen/${target})
-    set(generatedSourcesDir ${generatedSourcesIncludeDir}/gen)
-    file(MAKE_DIRECTORY ${generatedSourcesDir})
-
     get_filename_component(basename ${script} NAME_WLE)
     cmake_path(ABSOLUTE_PATH script OUTPUT_VARIABLE scriptAbsolute)
 
@@ -43,24 +39,23 @@ function(_pssfg_add_gen_source target script)
 
     set(generatedSourcesAbsolute)
     foreach (filename ${generatedSources})
-        list(APPEND generatedSourcesAbsolute "${generatedSourcesDir}/${filename}")
+        list(APPEND generatedSourcesAbsolute "${outputDirectory}/${filename}")
     endforeach ()
 
-    file(MAKE_DIRECTORY "${generatedSourcesDir}")
+    file(MAKE_DIRECTORY ${outputDirectory})
 
     add_custom_command(OUTPUT ${generatedSourcesAbsolute}
                        DEPENDS ${scriptAbsolute} ${_pssfg_DEPENDS}
                        COMMAND ${PystencilsSfg_PYTHON_INTERPRETER} ${scriptAbsolute} ${_pssfg_GENERATOR_ARGS} ${_pssfg_USER_ARGS}
-                       WORKING_DIRECTORY "${generatedSourcesDir}")
+                       WORKING_DIRECTORY "${outputDirectory}")
 
     target_sources(${target} PRIVATE ${generatedSourcesAbsolute})
-    target_include_directories(${target} PRIVATE ${generatedSourcesIncludeDir} ${_Pystencils_Include_Dir})
 endfunction()
 
 
 function(pystencilssfg_generate_target_sources TARGET)
     set(options)
-    set(oneValueArgs OUTPUT_MODE CONFIG_MODULE)
+    set(oneValueArgs OUTPUT_MODE CONFIG_MODULE OUTPUT_DIRECTORY)
     set(multiValueArgs SCRIPTS DEPENDS FILE_EXTENSIONS ARGS)
     cmake_parse_arguments(_pssfg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
 
@@ -93,6 +88,19 @@ function(pystencilssfg_generate_target_sources TARGET)
         endif()
     endif()
 
+    if(DEFINED _pssfg_OUTPUT_DIRECTORY)
+        cmake_path(IS_RELATIVE _pssfg_OUTPUT_DIRECTORY _pssfg_output_dir_is_relative)
+        if(_pssfg_output_dir_is_relative)
+            set(outputDirectory ${CMAKE_CURRENT_BINARY_DIR}/${_pssfg_OUTPUT_DIRECTORY})
+        else()
+            set(outputDirectory ${_pssfg_OUTPUT_DIRECTORY})
+        endif()
+    else()
+        set(generatedSourcesIncludeDir ${CMAKE_CURRENT_BINARY_DIR}/_gen/${TARGET})
+        set(outputDirectory ${generatedSourcesIncludeDir}/gen)
+        target_include_directories(${TARGET} PRIVATE ${generatedSourcesIncludeDir})
+    endif()
+
     if(DEFINED _pssfg_FILE_EXTENSIONS)
         string(JOIN "," extensionsString ${_pssfg_FILE_EXTENSIONS})
 
@@ -106,11 +114,13 @@ function(pystencilssfg_generate_target_sources TARGET)
 
     foreach(codegenScript ${_pssfg_SCRIPTS})
         _pssfg_add_gen_source(
-            ${TARGET} ${codegenScript}
+            ${TARGET} ${codegenScript} ${outputDirectory}
             GENERATOR_ARGS ${generatorArgs}
             USER_ARGS ${userArgs}
             DEPENDS ${_pssfg_DEPENDS}
         )
     endforeach()
+
+    target_include_directories(${TARGET} PRIVATE ${_Pystencils_Include_Dir})
     
 endfunction()
diff --git a/tests/integration/cmake_project/CMakeLists.txt b/tests/integration/cmake_project/CMakeLists.txt
index e4470c9e0cab8604dcbbdcff9f424d81655583fa..4c15d570dfa287e861ac710b59a66dc6ae28da88 100644
--- a/tests/integration/cmake_project/CMakeLists.txt
+++ b/tests/integration/cmake_project/CMakeLists.txt
@@ -37,3 +37,10 @@ pystencilssfg_generate_target_sources(
     ARGS apples bananas unicorns
     OUTPUT_MODE header-only
 )
+
+pystencilssfg_generate_target_sources(
+    TestApp
+    SCRIPTS CustomDirTest.py
+    OUTPUT_DIRECTORY my-output
+    OUTPUT_MODE header-only
+)
diff --git a/tests/integration/cmake_project/CustomDirTest.py b/tests/integration/cmake_project/CustomDirTest.py
new file mode 100644
index 0000000000000000000000000000000000000000..d99e6de9d4ab03d3105780d00d038aff6cbd0d7d
--- /dev/null
+++ b/tests/integration/cmake_project/CustomDirTest.py
@@ -0,0 +1,4 @@
+from pystencilssfg import SourceFileGenerator
+
+with SourceFileGenerator() as sfg:
+    sfg.code("#define NOTHING")
diff --git a/tests/integration/test_cmake.py b/tests/integration/test_cmake.py
index 1f0d9b2dc55a6be773a87b5ecd9b937dd2d08251..94853ac1b8143ff7149e5a268e086decfed1af61 100644
--- a/tests/integration/test_cmake.py
+++ b/tests/integration/test_cmake.py
@@ -45,3 +45,7 @@ def test_cmake_project(tmp_path, config_source):
     assert 'arg0 = "apples";' in content
     assert 'arg1 = "bananas";' in content
     assert 'arg2 = "unicorns";' in content
+
+    custom_dir_output = tmp_path / "my-output" / "CustomDirTest.hpp"
+    assert custom_dir_output.exists()
+    assert "#define NOTHING" in custom_dir_output.read_text()