diff --git a/CMakeLists.txt b/CMakeLists.txt
index d5ff07305cb4f88bf378c626121e1ce6c06dd07d..deade37d1a4999833a15faf8f8f62535d0964cf6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -126,7 +126,7 @@ if (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")
         -Weverything
 
         # Disable some things because we want C++20 and don't constrol some Qt
-        # generated files...
+        # generated files... Also permit VLA
         -Wno-c++98-compat -Wno-c++98-c++11-c++14-c++17-compat-pedantic
         -Wno-extra-semi-stmt
         -Wno-redundant-parens
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a28934fb8dbc059e39cd1acd2f66e68515d05f29..9c614dc0d12264e326bb42721661fa4fa61e8c7c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -54,6 +54,4 @@ Some of the feature may not be
 used or not already be in use for multiple reasons:
 
 - the new C++20 modules feature is not used because I don't know how it
-  will play with `moc`. It's not supported by clangd for now anyway
-
-
+  will play with `moc`. It's not supported by clangd for now anyway.
diff --git a/rsc/VivyRessources.qrc b/rsc/VivyRessources.qrc
index 542123ecd6cfc6901242cd5182f3821252f809ce..9078302c70ef82f2cf1a5eda9c0d82fc0b3ec51a 100644
--- a/rsc/VivyRessources.qrc
+++ b/rsc/VivyRessources.qrc
@@ -24,6 +24,7 @@
 
     <!-- Lua scripts -->
     <file>lua/lib.lua</file>
+    <file>lua/module.lua</file>
 </qresource>
 
 <!-- Breeze icons, LGPL-3.0 Licence -->
diff --git a/rsc/lua/lib.lua b/rsc/lua/lib.lua
index d21e6e14d3c11124043a792c0f74d86b9cb08a86..fb3f7efe7338c8f7be4aa9da7976dccdb82a1aac 100644
--- a/rsc/lua/lib.lua
+++ b/rsc/lua/lib.lua
@@ -5,86 +5,89 @@
     includes some exposed Vivy API things, enums and object to the user.
 ]]
 
+function getrometatable(tbl, tablename)
+    local errormsg = tablename ~= nil
+        and "attempt to update " .. tablename .. " table"
+        or  "attempt to update a read only table"
+    return {
+        __index     = tbl,
+        __metatable = false,
+        __newindex  = function (t, k, v)
+            error(errormsg, 2)
+        end
+    }
+end
+
+function setrometatable(tbl, tblname) setmetatable(tbl, getrometatable(tbl, tblname)) end
+function generalize(tbl) for k, v in pairs(tbl) do _G[k] = v end end
+
 -- Create an enum
 function enum(tbl)
     local length = #tbl
     for i = 1, length do
         local v = tbl[i]
-        tbl[v]  = i
+        tbl[v]  = v
     end
-
-    local mt = {
-        __index = tbl,
-        __newindex = function (t, k, v)
-            error("attempt to update an enum table", 2)
-        end
-    }
-
-    setmetatable(tbl, mt)
+    setrometatable(tbl, "an enum")
     return tbl
 end
 
--- Things used to iterate over with a job
+-- Enums
 Iterable = enum {
     "LINE",
-    "SYLLABE"
+    "SYLLABE",
 }
 
--- Table: ReservedKeywords["keyword"] = "type"
-local ReservedKeywords = {
-    ["Vivy"] = "table"
+StdType = enum {
+    "NUMBER",
+    "COLOR",
+    "BOOLEAN",
+    "STRING",
 }
 
 -- Vivy global object construction
-Vivy = {
-    -- Standard libraries
-    ["math"]   = math,
-    ["string"] = string,
-    ["utf8"]   = utf8,
-    ["table"]  = table,
-
-    -- Custom things
-    ["Iterable"] = Iterable,
-}
-
-version = {
-    ["MAJOR"] = 0,
-    ["MINOR"] = 1,
-    ["PATCH"] = 0,
-    ["LABEL"] = "alpha",
-}
+os, io, debug   = nil, nil, nil
+Vivy            = { }
+Version         = { MAJOR = 0, MINOR = 1, PATCH = 0, LABEL = "alpha" }
+Vivy["___Fun"]  = FreeFunctions()
 
-Vivy["Version"] = setmetatable(version, {
+Vivy["Version"] = setmetatable(Version, {
     __tostring = function ()
         return string.format("%d.%d.%d-%s",
-            Vivy.Version.MAJOR,
-            Vivy.Version.MINOR,
-            Vivy.Version.PATCH,
-            Vivy.Version.LABEL)
+        Version.MAJOR,
+        Version.MINOR,
+        Version.PATCH,
+        Version.LABEL)
+    end,
+    __index     = Version,
+    __metatable = false,
+    __newindex  = function (t, k, v)
+        error("attempt to update the version table", 2)
     end
 })
 
--- In case we are able to do the _ENV <- Vivy one day...
-Vivy["Vivy"] = Vivy
+function Vivy:global () return Vivy.___Fun end
 
-Vivy = setmetatable(Vivy, {
-    __newindex = function (t, k, v)
-        error("you can't set new keys in the Vivy table")
+function Vivy:module (tbl)
+    if type(tbl) == "table" and #tbl == 1 then
+        return Vivy.___Fun.getModule(tbl[1])
+    elseif type(tbl) == "string" then
+        return Vivy.___Fun.getModule(tbl)
+    else
+        error("Invalid argument, you should pass a single string", 2)
     end
-})
+end
+
+function Vivy:newColor (colorString)
+    -- Not implemented
+    return false
+end
 
--- Hide the io, os and debug libraries
-io    = nil
-os    = nil
-debug = nil
+-- In case we are able to do the _ENV <- Vivy one day...
+Vivy["Vivy"] = Vivy
 
-local _ENV = setmetatable(Vivy, {
-    __newindex = function (t, k, v)
-        -- Use the ReservedKeywords table here
-        if (k ~= "Vivy") then
-            rawset(t, k, v)
-        else
-            error("Try to assign something to the protected global symbol 'Vivy'")
-        end
-    end
-})
+-- Protect the content of Vivy, for now we are unable to protect the
+-- Vivy keyword...
+setrometatable(Vivy)
+generalize(Iterable)
+print = Vivy.___Fun.print
diff --git a/rsc/lua/module.lua b/rsc/lua/module.lua
new file mode 100644
index 0000000000000000000000000000000000000000..3468636a7168c7a2344ea5bb0c52aab401358233
--- /dev/null
+++ b/rsc/lua/module.lua
@@ -0,0 +1,121 @@
+--[[
+    Utilities to handle the module declaration files.
+]]
+
+-- Check if the type of the argument is 'table'
+function ___checkTable(tbl)
+    if type(tbl) ~= "table" then
+        error("The passed argument is of type " .. type(tbl) .. " and not table", 3)
+    end
+end
+
+-- Local private function: try to import options from a table into a job or a
+-- module declaration.
+function ___tryImportOptions(thing, tbl)
+    ___checkTable(tbl)
+    if tbl["options"] then
+        ___checkTable(tbl.options)
+        thing:setOptions(tbl.options)
+    end
+end
+
+-- The option declaration block:
+--      DeclareOption { opt1 = val1, opt2 = val2 }
+function DeclareOption(opt)
+    ___checkTable(opt)
+    local option = OptionDeclaration()
+    for o, val in pairs(opt) do
+        option:addOptionContent(o, val)
+    end
+    return option
+end
+
+-- The function declaration block
+--      DeclareFunction { name }
+function DeclareFunction(tbl)
+    ___checkTable(tbl)
+    if #tbl == 1 and type(tbl[1]) == "string" then
+        local func = FunctionDeclaration()
+        func:setName(tbl[1])
+        return func
+    end
+    error("invalid number of arguments in the DeclareFuncton block", 2)
+end
+
+-- The function import block
+--      ImportFunction { script, name }
+function ImportFunction(tbl)
+    ___checkTable(tbl)
+    if #tbl == 2 and type(tbl[1]) == "string" and type(tbl[2]) == "string" then
+        local func = FunctionDeclaration()
+        func:setImportFromScript(tbl[1])
+        func:setName(tbl[2])
+        return func
+    end
+    error("invalid number of arguments in the ImportFunction block", 2)
+end
+
+-- The job declaration block
+--      DeclareJob { name }
+--      DeclareJob { name, options = { ... } }
+function DeclareJob(tbl)
+    ___checkTable(tbl)
+    if #tbl >= 1 and #tbl <= 2 and tbl[1] and type(tbl[1]) == "string" then
+        local job = JobDeclaration()
+        job:setName(tbl[1])
+        ___tryImportOptions(job, tbl)
+        return job
+    end
+    error("invalid DeclareJob block, need at least a name", 2)
+end
+
+-- The job import block
+--      ImportJob { script, job }
+--      ImportJob { script, job, options = { ... } }
+function ImportJob(tbl)
+    ___checkTable(tbl)
+    if #tbl >= 2 and #tbl <= 3 and type(tbl[1]) == "string" and type(tbl[2]) == "string" then
+        local job = JobDeclaration()
+        job:setName(tbl[2])
+        job:setImportFromScript(tbl[1])
+        ___tryImportOptions(job, tbl)
+        return job
+    end
+    error("invalid ImportJob block, need at least a script and a name", 2)
+end
+
+-- The module declaration block
+--      DeclareModule {
+--          name = ..., description = ..., author = ...,
+--          revision = ..., imports = { ... }, options = { ... },
+--          fuctions = { ... }, jobs = { ... }
+--      }
+--      DeclareModule { name = ..., author = ... }
+function DeclareModule(tbl)
+    ___checkTable(tbl)
+    if not tbl.name   then error("Invalid DeclareModule block, Missing name",   2) end
+    if not tbl.author then error("Invalid DeclareModule block, Missing author", 2) end
+    local module = ModuleDeclaration()
+
+    -- Standard things
+    module:setName(tbl.name)
+    module:setAuthorName(tbl.author)
+
+    -- Optional simple things
+    if tbl.description then module:setDescription(tbl.description) end
+    if tbl.revision    then module:setRevision(tbl.revision) end
+    if tbl.imports     then module:setImports(tbl.imports) end
+
+    -- A little more complicated thing
+    if tbl.jobs      then module:setJobs(tbl.jobs) end
+    if tbl.functions then module:setFunctions(tbl.functions) end
+
+    ___tryImportOptions(module, tbl)
+    -- Register jobs and functions in runtime now that they are added to the module!
+    for _, job in ipairs(tbl.jobs)      do job:pushToRuntime() end
+    for _, fun in ipairs(tbl.functions) do fun:pushToRuntime() end
+
+    -- Register the module!
+    module:pushToRuntime()
+    return module
+end
diff --git a/src/Lib/Script/CRTPLuaScriptObject.cc b/src/Lib/Script/CRTPLuaScriptObject.cc
deleted file mode 100644
index f720dff6032a5d69f86645ecc5340202dd98fae2..0000000000000000000000000000000000000000
--- a/src/Lib/Script/CRTPLuaScriptObject.cc
+++ /dev/null
@@ -1,18 +0,0 @@
-#include "CRTPLuaScriptObject.hh"
-#include "lua.hpp"
-
-// SampleObject implementation
-using namespace Vivy::Script;
-
-SampleObject::SampleObject() noexcept { fprintf(stderr, "CREATE SampleObject\n"); }
-
-SampleObject::~SampleObject() noexcept { fprintf(stderr, "DESTROY SampleObject\n"); }
-
-int
-SampleObject::foo(lua_State *L) noexcept
-{
-    SampleObject *obj = CHECK(L, 1);
-    double bar        = luaL_checknumber(L, 2);
-    fprintf(stderr, "SampleObject(%p)::foo -> %f\n", static_cast<void *>(obj), bar);
-    return 0;
-}
diff --git a/src/Lib/Script/CRTPLuaScriptObject.hh b/src/Lib/Script/CRTPLuaScriptObject.hh
index b622d71fbb4459b156d3a44676e2c265e94dbd2a..8e9920da1f8c51f0a158168bd9ebcda2c73d5e7c 100644
--- a/src/Lib/Script/CRTPLuaScriptObject.hh
+++ b/src/Lib/Script/CRTPLuaScriptObject.hh
@@ -1,6 +1,7 @@
 #pragma once
 
 #include "LuaContext.hh"
+#include "ScriptOption.hh"
 
 #include "lua.hpp"
 #include <vector>
@@ -9,54 +10,130 @@
 namespace Vivy::Script
 {
 // clang-format off
-#define LUA_DECL_METHOD(class, name) luaL_Reg{ #name, class ::name   }
-#define LUA_DECL_CREATE(class)       luaL_Reg{ "new", class ::CREATE }
+#define LUA_DECL_METHOD(class, name)                luaL_Reg{ #name,  class ::name   }
+#define LUA_DECL_NAMED_METHOD(class, method, name)  luaL_Reg{ method, class ::name   }
+#define LUA_DECL_META_METHOD(class, meta, name)     luaL_Reg{ meta,   class ::name   }
+#define LUA_DECL_CREATE(class)                      luaL_Reg{ "new",  class ::CREATE }
+#define LUA_RETURN_NOTHING(L)                       { lua_settop(L, 0); return 0; }
+// clang-format on
 
 #define script_class(theClassName) \
     class theClassName final : public CRTPLuaScriptObject<theClassName>
 
-#define LUA_SCRIPTABLE_CLASS(theClassName)                     \
-    VIVY_UNMOVABLE_OBJECT(theClassName)                        \
-    static constexpr char className[] = #theClassName;         \
-    friend CRTPLuaScriptObject<theClassName>;
-// clang-format on
+#define method_list static constexpr inline auto
+
+#define LUA_SCRIPTABLE_CLASS(theClassName)                               \
+    VIVY_UNMOVABLE_OBJECT(theClassName)                                  \
+    static constexpr char className[] = #theClassName;                   \
+    friend CRTPLuaScriptObject<theClassName>;                            \
+    theClassName(lua_State *L) noexcept                                  \
+    {                                                                    \
+        err(LuaContext::getContext(L))                                   \
+            << "Create instance of " << theClassName::className << "\n"; \
+    }                                                                    \
+    ~theClassName() noexcept {}
+
+// The type of the thing that a job iterate over
+enum class JobIteratorType { Line = 1, Syllabe = 2 };
+static inline JobIteratorType
+getJobIteratorTypeFromString(const std::string_view it) noexcept
+{
+    if (it == "LINE")
+        return JobIteratorType::Line;
+    else if (it == "SYLLABE")
+        return JobIteratorType::Syllabe;
+    return static_cast<JobIteratorType>(0); // Invalid!
+}
 
 // CRTP to expose objects to Lua
 template <class Object> class CRTPLuaScriptObject {
 protected:
-    static int GC(lua_State *L) noexcept
+    static void __attribute__((__noreturn__))
+    luaGlobalError(lua_State *const L, const std::string &str) noexcept
+    {
+        const auto *const context = LuaContext::getContext(L);
+        lua_pushfstring(L, "%s:0:%s", context->getCurrentLuaFile().c_str(), str.c_str());
+        lua_error(L);
+    }
+
+    static bool PushFunctionFromRegistry(lua_State *const L, const int key) noexcept
     {
-        const Object *obj = reinterpret_cast<const Object *>(lua_topointer(L, 1));
-        puts("CALL DESTRUCTOR");
+        if (key >= 0) {
+            lua_rawgeti(L, LUA_REGISTRYINDEX, key);
+            return true;
+        } else {
+            lua_pushnil(L);
+            return false;
+        }
+    }
+
+    static void IterateOverArray(lua_State *const L, const int tblIndex,
+                                 const auto callback) noexcept
+    {
+        luaL_checktype(L, tblIndex, LUA_TTABLE);
+        const std::size_t count = lua_rawlen(L, tblIndex);
+        if (count >= std::numeric_limits<int>::max())
+            luaL_error(L, "To many items (%lu) in table to iterate over!", count);
+
+        const int countInt = static_cast<const int>(count);
+        for (int i = 1; i <= countInt; ++i) {
+            luaL_checktype(L, tblIndex, LUA_TTABLE);
+            lua_pushinteger(L, i);     // i -> top stack
+            lua_gettable(L, tblIndex); // T[i] -> top stack and pop i
+
+            if (lua_type(L, -1) == LUA_TNIL)
+                break; // Sentinel check
+
+            // If the callback didn't pop the object form the stack, pop
+            // it ourself. The value will be accessible at the top of the
+            // stack (index -1).
+            if (!callback())
+                lua_pop(L, -1);
+        }
+    }
+
+    static int GC(lua_State *const L) noexcept
+    {
+        const Object *const obj = reinterpret_cast<const Object *>(lua_topointer(L, 1));
+        auto *context           = LuaContext::getContext(L);
+        err(context) << "Call destructor for " << Object::className << "\n";
         obj->~Object();
         return 0;
     }
 
-    static int CREATE(lua_State *L) noexcept
+    static int TOSTRING(lua_State *const L) noexcept
     {
-        Object *obj = reinterpret_cast<Object *>(lua_newuserdata(L, sizeof(Object)));
-        puts("CALL CONSTRUCTOR");
-        new (obj) Object;
+        lua_pushfstring(L, "%s { }", Object::className);
+        return 1;
+    }
+
+    static int CREATE(lua_State *const L) noexcept
+    {
+        lua_settop(L, 0);
+        Object *const obj = reinterpret_cast<Object *>(lua_newuserdata(L, sizeof(Object)));
+        new (obj) Object(L);
         luaL_getmetatable(L, Object::className);
-        lua_setmetatable(L, -2);
+        lua_setmetatable(L, 1);
         return 1;
     }
 
-    static Object *CHECK(lua_State *L, int index) noexcept
+    static const std::string_view CHECK_STRING_VIEW(lua_State *const L, int index) noexcept
     {
-        luaL_checktype(L, index, LUA_TUSERDATA);
-        Object *obj = reinterpret_cast<Object *>(luaL_checkudata(L, index, Object::className));
-        if (obj == nullptr) {
-            puts("TYPE ERROR");
-            luaL_typeerror(L, index, Object::className);
-        }
-        return obj;
+        std::size_t len       = 0;
+        const char *const str = luaL_checklstring(L, index, &len);
+        return std::string_view(str, len);
     }
 
-    static inline constexpr luaL_Reg luaRegDefaultGC = { "__gc", GC };
+    static inline constexpr luaL_Reg luaRegDefaultGC       = { "__gc", GC };
+    static inline constexpr luaL_Reg luaRegDefaultTOSTRING = { "__tostring", TOSTRING };
 
 public:
-    static void Register(lua_State *L) noexcept
+    static Object *CHECK(lua_State *const L, const int index) noexcept
+    {
+        return reinterpret_cast<Object *>(luaL_checkudata(L, index, Object::className));
+    }
+
+    static void Register(lua_State *const L) noexcept
     {
         // Fill the method table, newclass = {}
         lua_newtable(L);
@@ -76,7 +153,6 @@ public:
             lua_settable(L, metatable);
 
             for (const luaL_Reg &meta : Object::metaMethods) {
-                fprintf(stderr, "REGISTER %s\n", meta.name);
                 lua_pushstring(L, meta.name);
                 lua_pushcfunction(L, meta.func);
                 lua_settable(L, metatable);
@@ -87,7 +163,6 @@ public:
 
         // Fill the method table with specified methods
         for (const luaL_Reg &method : Object::methods) {
-            fprintf(stderr, "REGISTER %s\n", method.name);
             lua_pushstring(L, method.name);
             lua_pushcfunction(L, method.func);
             lua_settable(L, methodtable);
@@ -97,22 +172,172 @@ public:
         lua_pop(L, 1);                              // drop method-table
         lua_register(L, Object::className, CREATE); // Register the create method
     }
+
+private:
+    // The user value count
+    int userValueCount{ 0 };
+
+protected:
+    // Add a user value to this object. Do not used lua_setuservalue or
+    // lua_setiuservalue directly, use this proxy method.
+    void addUserValue(lua_State *const L, const int index) noexcept
+    {
+        lua_setiuservalue(L, index, ++userValueCount);
+    }
+};
+
+// Option declaration
+script_class (OptionDeclaration) {
+    LUA_SCRIPTABLE_CLASS(OptionDeclaration)
+
+    static int addOptionContent(lua_State *const) noexcept;
+
+    static int TOSTRING(lua_State *const) noexcept;
+
+    method_list metaMethods = {
+        luaRegDefaultGC,
+        LUA_DECL_META_METHOD(OptionDeclaration, "__tostring", TOSTRING),
+    };
+    method_list methods = { LUA_DECL_METHOD(OptionDeclaration, addOptionContent),
+                            LUA_DECL_CREATE(OptionDeclaration) };
+
+public:
+    // Different option types
+    std::unordered_map<std::string, ScriptOption> options;
+};
+
+// Job declaration
+script_class (JobDeclaration) {
+    LUA_SCRIPTABLE_CLASS(JobDeclaration)
+
+    static int setName(lua_State *) noexcept;
+    static int setOptions(lua_State *) noexcept;
+    static int setImportFromScript(lua_State *) noexcept;
+
+    static int pushToRuntime(lua_State *const) noexcept;
+
+    static int TOSTRING(lua_State *const) noexcept;
+
+    method_list metaMethods = { luaRegDefaultGC,
+                                LUA_DECL_META_METHOD(JobDeclaration, "__tostring", TOSTRING) };
+    method_list methods     = { LUA_DECL_METHOD(JobDeclaration, setName),
+                            LUA_DECL_METHOD(JobDeclaration, setOptions),
+                            LUA_DECL_METHOD(JobDeclaration, setImportFromScript),
+                            LUA_DECL_METHOD(JobDeclaration, pushToRuntime),
+                            LUA_DECL_CREATE(JobDeclaration) };
+
+    bool isImported{ false };
+    int functionRegisterKey{ -1 };
+    JobIteratorType iterationType{ static_cast<JobIteratorType>(0) }; // Invalid by default
+
+public:
+    std::string name{};
+    std::string parentScript{};
+    std::vector<OptionDeclaration *> options{};
+
+    bool isLocalToModule() const noexcept;
+    void pushLuaFunction(lua_State *const) const noexcept;
+    void getLuaFunctionFromStack(lua_State *const, const JobIteratorType) noexcept;
 };
 
-// Default sample exposer for lua scripts
-script_class (SampleObject) {
-    // Specify that it's a Lua object
-    LUA_SCRIPTABLE_CLASS(SampleObject)
+// Function declaration
+script_class (FunctionDeclaration) {
+    LUA_SCRIPTABLE_CLASS(FunctionDeclaration)
+
+    static int setName(lua_State *const) noexcept;
+    static int setImportFromScript(lua_State *const) noexcept;
+
+    static int pushToRuntime(lua_State *const) noexcept;
+
+    static int TOSTRING(lua_State *const) noexcept;
+
+    method_list metaMethods = { luaRegDefaultGC,
+                                LUA_DECL_META_METHOD(FunctionDeclaration, "__tostring", TOSTRING) };
+    method_list methods     = { LUA_DECL_METHOD(FunctionDeclaration, setName),
+                            LUA_DECL_METHOD(FunctionDeclaration, setImportFromScript),
+                            LUA_DECL_METHOD(FunctionDeclaration, pushToRuntime),
+                            LUA_DECL_CREATE(FunctionDeclaration) };
+
+    bool isImported{ false };
+    int functionRegisterKey{ -1 };
+
+public:
+    std::string name{};
+    std::string parentScript{};
+
+    bool isLocalToModule() const noexcept;
+    void pushLuaFunction(lua_State *const) const noexcept;
+    void getLuaFunctionFromStack(lua_State *const) noexcept;
+};
+
+// Module declaration
+script_class (ModuleDeclaration) {
+    LUA_SCRIPTABLE_CLASS(ModuleDeclaration)
+
+    static int setName(lua_State *const) noexcept;
+    static int setAuthorName(lua_State *const) noexcept;
+    static int setRevision(lua_State *const) noexcept;
+    static int setDescription(lua_State *const) noexcept;
+    static int setImports(lua_State *const) noexcept;
+    static int setOptions(lua_State *const) noexcept;
+    static int setFunctions(lua_State *const) noexcept;
+    static int setJobs(lua_State *const) noexcept;
+
+    static int pushToRuntime(lua_State *const) noexcept;
+
+    static int importMethod(lua_State *const) noexcept;
+    static int exportMethod(lua_State *const) noexcept;
+
+    method_list metaMethods = { luaRegDefaultGC };
+    method_list methods     = {
+        LUA_DECL_METHOD(ModuleDeclaration, setName),
+        LUA_DECL_METHOD(ModuleDeclaration, setDescription),
+        LUA_DECL_METHOD(ModuleDeclaration, setRevision),
+        LUA_DECL_METHOD(ModuleDeclaration, setAuthorName),
+        LUA_DECL_METHOD(ModuleDeclaration, setImports),
+        LUA_DECL_METHOD(ModuleDeclaration, setOptions),
+        LUA_DECL_METHOD(ModuleDeclaration, setFunctions),
+        LUA_DECL_METHOD(ModuleDeclaration, setJobs),
+        LUA_DECL_METHOD(ModuleDeclaration, pushToRuntime),
+        LUA_DECL_NAMED_METHOD(ModuleDeclaration, "import", importMethod),
+        LUA_DECL_NAMED_METHOD(ModuleDeclaration, "export", exportMethod),
+        LUA_DECL_CREATE(ModuleDeclaration),
+    };
+
+    // Will be resolved later
+    std::vector<std::string> importNames{};
+    std::vector<const ModuleDeclaration *> importedModules{};
+
+    void validateModule(lua_State *const) const noexcept;
+    int exportFunction(lua_State *const, const int tableIndex, const std::string_view) noexcept;
+    int exportJob(lua_State *const, const int tableIndex, const std::string_view,
+                  const std::string_view) noexcept;
+
+public:
+    std::string moduleName{};
+    std::string authorName{};
+    std::string revision{};
+    std::string moduleDescription{};
+    std::vector<OptionDeclaration *> moduleOptions{};
+    std::vector<FunctionDeclaration *> moduleFunctions{};
+    std::vector<JobDeclaration *> moduleJobs{};
+
+    bool resolvModules(LuaContext *const) noexcept;
+};
 
-    // CTors and DTors
-    SampleObject() noexcept;
-    ~SampleObject() noexcept;
+// Holds all the free functions (well, not really free functions here...)
+script_class (FreeFunctions) {
+    LUA_SCRIPTABLE_CLASS(FreeFunctions)
 
-    // The methods must be static with Lua interface
-    static int foo(lua_State * L) noexcept;
+    static int print(lua_State *const) noexcept;
+    static int getModule(lua_State *const) noexcept;
+    static int start(lua_State *const) noexcept;
+    static int finish(lua_State *const) noexcept;
 
-    // Meta and method tables, Lua specific thing
-    static constexpr inline auto metaMethods = { luaRegDefaultGC, LUA_DECL_CREATE(SampleObject) };
-    static constexpr inline auto methods     = { LUA_DECL_METHOD(SampleObject, foo) };
+    method_list metaMethods = { luaRegDefaultGC };
+    method_list methods     = { LUA_DECL_METHOD(FreeFunctions, getModule),
+                            LUA_DECL_METHOD(FreeFunctions, start),
+                            LUA_DECL_METHOD(FreeFunctions, finish),
+                            LUA_DECL_METHOD(FreeFunctions, print), LUA_DECL_CREATE(FreeFunctions) };
 };
 }
diff --git a/src/Lib/Script/CRTPLuaScriptObject/FreeFunctions.cc b/src/Lib/Script/CRTPLuaScriptObject/FreeFunctions.cc
new file mode 100644
index 0000000000000000000000000000000000000000..b2c3127aa97ce23e229ac651ca7b6c7ae7c6a5fe
--- /dev/null
+++ b/src/Lib/Script/CRTPLuaScriptObject/FreeFunctions.cc
@@ -0,0 +1,39 @@
+#include "../CRTPLuaScriptObject.hh"
+#include "lua.hpp"
+#include <QFileInfo>
+#include <QString>
+
+using namespace Vivy::Script;
+
+int
+FreeFunctions::print(lua_State *const L) noexcept
+{
+    const std::string_view arg = CHECK_STRING_VIEW(L, 1);
+    out(LuaContext::getContext(L)) << "OUT: " << arg << "\n";
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+FreeFunctions::getModule(lua_State *const L) noexcept
+{
+    const auto *const context                           = LuaContext::getContext(L);
+    const std::string_view modName                      = CHECK_STRING_VIEW(L, 1);
+    [[maybe_unused]] const ModuleDeclaration *const mod = context->getModule(modName);
+    return 1;
+}
+
+int
+FreeFunctions::start(lua_State *const L) noexcept
+{
+    lua_settop(L, 0);
+    lua_pushinteger(L, 0);
+    return 1;
+}
+
+int
+FreeFunctions::finish(lua_State *const L) noexcept
+{
+    lua_settop(L, 0);
+    lua_pushinteger(L, 0);
+    return 1;
+}
diff --git a/src/Lib/Script/CRTPLuaScriptObject/FunctionDeclaration.cc b/src/Lib/Script/CRTPLuaScriptObject/FunctionDeclaration.cc
new file mode 100644
index 0000000000000000000000000000000000000000..82afe1daa0b10b2824dbd4b9b1fbda7588f9f371
--- /dev/null
+++ b/src/Lib/Script/CRTPLuaScriptObject/FunctionDeclaration.cc
@@ -0,0 +1,71 @@
+#include "../CRTPLuaScriptObject.hh"
+#include "lua.hpp"
+#include <QFileInfo>
+#include <QString>
+
+using namespace Vivy::Script;
+
+int
+FunctionDeclaration::TOSTRING(lua_State *const L) noexcept
+{
+    FunctionDeclaration *const self = CHECK(L, 1);
+    std::string str(className);
+    str += std::string_view(" { ");
+    str += self->parentScript.empty() ? self->name : self->parentScript + "::" + self->name;
+    str += std::string_view(" }");
+    lua_pushstring(L, str.c_str());
+    return 1;
+}
+
+int
+FunctionDeclaration::setName(lua_State *const L) noexcept
+{
+    FunctionDeclaration::CHECK(L, 1)->name = CHECK_STRING_VIEW(L, 2);
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+FunctionDeclaration::setImportFromScript(lua_State *const L) noexcept
+{
+    auto *const fun   = FunctionDeclaration::CHECK(L, 1);
+    fun->parentScript = CHECK_STRING_VIEW(L, 2);
+    fun->isImported   = true;
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+FunctionDeclaration::pushToRuntime(lua_State *const L) noexcept
+{
+    FunctionDeclaration *const self = CHECK(L, 1);
+    auto *const context             = LuaContext::getContext(L);
+
+    lua_settop(L, 1);
+
+    if (self->isImported)
+        err(context) << "The function \"" << self->parentScript << "::" << self->name
+                     << "\" is an imported function\n";
+
+    else if (context->registerDeclaration(self) == LuaContext::Code::Error)
+        context->setFailed("Failed to register function " + self->name);
+
+    LUA_RETURN_NOTHING(L);
+}
+
+void
+FunctionDeclaration::pushLuaFunction(lua_State *const L) const noexcept
+{
+    [[maybe_unused]] bool ok = PushFunctionFromRegistry(L, functionRegisterKey);
+}
+
+bool
+FunctionDeclaration::isLocalToModule() const noexcept
+{
+    return parentScript.size() == 0;
+}
+
+void
+FunctionDeclaration::getLuaFunctionFromStack(lua_State *const L) noexcept
+{
+    luaL_checktype(L, -1, LUA_TFUNCTION);
+    functionRegisterKey = luaL_ref(L, LUA_REGISTRYINDEX);
+}
diff --git a/src/Lib/Script/CRTPLuaScriptObject/JobDeclaration.cc b/src/Lib/Script/CRTPLuaScriptObject/JobDeclaration.cc
new file mode 100644
index 0000000000000000000000000000000000000000..39c676d96f83d05072484122761ea0b91dc34d59
--- /dev/null
+++ b/src/Lib/Script/CRTPLuaScriptObject/JobDeclaration.cc
@@ -0,0 +1,103 @@
+#include "../CRTPLuaScriptObject.hh"
+#include "lua.hpp"
+#include <QFileInfo>
+#include <QString>
+
+using namespace Vivy::Script;
+
+int
+JobDeclaration::TOSTRING(lua_State *const L) noexcept
+{
+    JobDeclaration *const self = CHECK(L, 1);
+    std::string str(className);
+    str += std::string_view(" { ");
+
+    str += self->parentScript.empty() ? self->name : self->parentScript + "::" + self->name;
+    str += ", options: [";
+
+    for (const OptionDeclaration *const opt : self->options) {
+        for (auto const &[name, _] : opt->options)
+            str += " " + name;
+    }
+
+    str += std::string_view(" ] }");
+    lua_pushstring(L, str.c_str());
+    return 1;
+}
+
+int
+JobDeclaration::setName(lua_State *L) noexcept
+{
+    JobDeclaration::CHECK(L, 1)->name = CHECK_STRING_VIEW(L, 2);
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+JobDeclaration::setOptions(lua_State *L) noexcept
+{
+    auto *const context       = LuaContext::getContext(L);
+    JobDeclaration *const job = JobDeclaration::CHECK(L, 1);
+    err(context) << "Set options for job '" << job->name << "'\n";
+
+    IterateOverArray(L, 2, [&context, job, L]() noexcept -> bool {
+        OptionDeclaration *opt = OptionDeclaration::CHECK(L, -1);
+
+        for (const auto &[name, value] : opt->options) {
+            err(context) << "\tregister option \"" << name << "\" with type \""
+                         << value.getSignature() << "\"\n";
+        }
+
+        job->options.push_back(opt);
+        job->addUserValue(L, 1); // Tie T[i] to job and pop stack
+        return true;             // Value was popped from the stack
+    });
+
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+JobDeclaration::setImportFromScript(lua_State *L) noexcept
+{
+    auto *const job   = JobDeclaration::CHECK(L, 1);
+    job->parentScript = CHECK_STRING_VIEW(L, 2);
+    job->isImported   = true;
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+JobDeclaration::pushToRuntime(lua_State *const L) noexcept
+{
+    JobDeclaration *const self = CHECK(L, 1);
+    auto *const context        = LuaContext::getContext(L);
+
+    lua_settop(L, 1);
+
+    if (self->isImported)
+        err(context) << "The job \"" << self->parentScript << "::" << self->name
+                     << "\" is an imported job\n";
+
+    else if (context->registerDeclaration(self) == LuaContext::Code::Error)
+        context->setFailed("Failed to register job " + self->name);
+
+    LUA_RETURN_NOTHING(L);
+}
+
+bool
+JobDeclaration::isLocalToModule() const noexcept
+{
+    return parentScript.size() == 0;
+}
+
+void
+JobDeclaration::pushLuaFunction(lua_State *const L) const noexcept
+{
+    [[maybe_unused]] bool ok = PushFunctionFromRegistry(L, functionRegisterKey);
+}
+
+void
+JobDeclaration::getLuaFunctionFromStack(lua_State *const L, const JobIteratorType itType) noexcept
+{
+    luaL_checktype(L, -1, LUA_TFUNCTION);
+    functionRegisterKey = luaL_ref(L, LUA_REGISTRYINDEX);
+    iterationType       = itType;
+}
diff --git a/src/Lib/Script/CRTPLuaScriptObject/ModuleDeclaration.cc b/src/Lib/Script/CRTPLuaScriptObject/ModuleDeclaration.cc
new file mode 100644
index 0000000000000000000000000000000000000000..d30eeee2c284e445eb5332e9db6ec1b4a1ae2656
--- /dev/null
+++ b/src/Lib/Script/CRTPLuaScriptObject/ModuleDeclaration.cc
@@ -0,0 +1,266 @@
+#include "../CRTPLuaScriptObject.hh"
+#include "lua.hpp"
+#include <QFileInfo>
+#include <QString>
+#include <cstring>
+
+using namespace Vivy::Script;
+
+bool
+ModuleDeclaration::resolvModules(LuaContext *const context) noexcept
+{
+    for (const auto &str : importNames) {
+        const ModuleDeclaration *const mod = context->getModule(str);
+        if (mod) {
+            importedModules.emplace_back(mod);
+        } else {
+            context->setFailed("Failed to find needed module: " + str);
+            return false;
+        }
+    }
+    return true;
+}
+
+int
+ModuleDeclaration::importMethod(lua_State *const L) noexcept
+{
+    auto *const context = LuaContext::getContext(L);
+    luaL_checktype(L, 2, LUA_TTABLE);
+
+    // Get the module name
+    lua_pushinteger(L, 1); // Stack is { top: int, table, self }
+    lua_gettable(L, 2);    // Stack is { top: str, table, self }
+    const std::string_view mod = CHECK_STRING_VIEW(L, 3);
+
+    // Import a module
+    context->getModule(mod); // Stack is { top: nil | module, str, table, self }
+
+    // Import a function if needed
+    if (lua_rawlen(L, 2) == 2) {
+        lua_pushinteger(L, 2); // Stack is { top: int, module, str, table, self }
+        lua_gettable(L, 2);    // Stack is { top: str, module, str, table, self }
+        const std::string_view func         = CHECK_STRING_VIEW(L, 5);
+        FunctionDeclaration *const function = context->getFunction(mod, func);
+        if (function)
+            function->pushLuaFunction(L);
+        // Stack is { top: nil | function, FunctionDeclaration, str, module, str, table, self }
+    }
+
+    // Top of stack is nil | function | module
+    return 1;
+}
+
+int
+ModuleDeclaration::exportMethod(lua_State *const L) noexcept
+{
+    ModuleDeclaration *const self = CHECK(L, 1);
+    luaL_checktype(L, 2, LUA_TTABLE);
+
+    // Stack will be { top: string, table, self }
+    lua_pushinteger(L, 1);
+    lua_gettable(L, 2);
+    const std::string_view objectName = CHECK_STRING_VIEW(L, -1);
+
+    const lua_Unsigned len = lua_rawlen(L, 2);
+    if (len == 2)
+        return self->exportFunction(L, 2, objectName);
+
+    else if (len == 3) {
+        lua_pushinteger(L, 2);
+        lua_gettable(L, 2);
+        const std::string_view jobItTypeStr = CHECK_STRING_VIEW(L, -1);
+        return self->exportJob(L, 2, objectName, jobItTypeStr);
+    }
+
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+ModuleDeclaration::exportFunction(lua_State *const L, const int tableIndex,
+                                  const std::string_view name) noexcept
+{
+    auto *const context             = LuaContext::getContext(L);
+    FunctionDeclaration *const func = context->getFunction(moduleName, name);
+    out(context) << "Export function " << moduleName << "::" << name << '\n';
+
+    lua_pushinteger(L, 2);
+    lua_gettable(L, tableIndex);
+    func->getLuaFunctionFromStack(L);
+
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+ModuleDeclaration::exportJob(lua_State *const L, const int tableIndex, const std::string_view name,
+                             const std::string_view itTypeStr) noexcept
+{
+    auto *const context             = LuaContext::getContext(L);
+    const JobIteratorType jobItType = getJobIteratorTypeFromString(itTypeStr);
+    JobDeclaration *const job       = context->getJob(moduleName, name);
+
+    if (jobItType != JobIteratorType::Line && jobItType != JobIteratorType::Syllabe) {
+        const std::string str(itTypeStr);
+        luaL_error(L, "The iteration type must be LINE or SYLLABE, got %s", str.c_str());
+    }
+
+    else {
+        out(context) << "Export job " << moduleName << "::" << name << "{" << itTypeStr << "}\n";
+
+        lua_pushinteger(L, 3);
+        lua_gettable(L, tableIndex);
+        job->getLuaFunctionFromStack(L, jobItType);
+    }
+
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+ModuleDeclaration::setName(lua_State *const L) noexcept
+{
+    auto *const self = ModuleDeclaration::CHECK(L, 1);
+    self->moduleName = CHECK_STRING_VIEW(L, 2);
+    Utils::trim(self->moduleName);
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+ModuleDeclaration::setAuthorName(lua_State *const L) noexcept
+{
+    auto *const self = ModuleDeclaration::CHECK(L, 1);
+    self->authorName = CHECK_STRING_VIEW(L, 2);
+    Utils::trim(self->authorName);
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+ModuleDeclaration::setRevision(lua_State *const L) noexcept
+{
+    auto *const self = ModuleDeclaration::CHECK(L, 1);
+    self->revision   = CHECK_STRING_VIEW(L, 2);
+    Utils::trim(self->revision);
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+ModuleDeclaration::setDescription(lua_State *const L) noexcept
+{
+    auto *const self        = ModuleDeclaration::CHECK(L, 1);
+    self->moduleDescription = CHECK_STRING_VIEW(L, 2);
+    Utils::trim(self->moduleDescription);
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+ModuleDeclaration::setImports(lua_State *const L) noexcept
+{
+    auto *const self = ModuleDeclaration::CHECK(L, 1);
+    IterateOverArray(L, 2, [self, L]() noexcept -> bool {
+        std::string import(CHECK_STRING_VIEW(L, -1));
+        Utils::trim(import);
+        self->importNames.emplace_back(import);
+        return false; // Value was not popped from stack!
+    });
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+ModuleDeclaration::setOptions(lua_State *const L) noexcept
+{
+    auto *const self = ModuleDeclaration::CHECK(L, 1);
+    IterateOverArray(L, 2, [self, L]() noexcept -> bool {
+        OptionDeclaration *const opt = OptionDeclaration::CHECK(L, -1);
+        self->moduleOptions.emplace_back(opt);
+        self->addUserValue(L, 1); // Tie the opt to the module
+        return true;              // The value was popped from the stack
+    });
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+ModuleDeclaration::setFunctions(lua_State *const L) noexcept
+{
+    auto *const self = ModuleDeclaration::CHECK(L, 1);
+    IterateOverArray(L, 2, [self, L]() noexcept -> bool {
+        FunctionDeclaration *const fun = FunctionDeclaration::CHECK(L, -1);
+        if (fun->isLocalToModule())
+            fun->parentScript = self->moduleName;
+        self->moduleFunctions.emplace_back(fun);
+        self->addUserValue(L, 1); // Tie the function to the module
+        return true;              // The value was popped from the stack
+    });
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+ModuleDeclaration::setJobs(lua_State *const L) noexcept
+{
+    auto *const self = ModuleDeclaration::CHECK(L, 1);
+    IterateOverArray(L, 2, [self, L]() noexcept -> bool {
+        JobDeclaration *const job = JobDeclaration::CHECK(L, -1);
+        if (job->isLocalToModule())
+            job->parentScript = self->moduleName;
+        self->moduleJobs.emplace_back(job);
+        self->addUserValue(L, 1); // Tie the job to the module
+        return true;              // The value was popped from the stack
+    });
+    LUA_RETURN_NOTHING(L);
+}
+
+int
+ModuleDeclaration::pushToRuntime(lua_State *const L) noexcept
+{
+    ModuleDeclaration *const self = CHECK(L, 1);
+    auto *const context           = LuaContext::getContext(L);
+
+    self->importNames.emplace_back(self->moduleName);   // You can use localy defined things
+    Utils::uniqAndSort<std::string>(self->importNames); // Better for all std algorithms
+
+    self->validateModule(L); // May abort the LUA context
+    err(context) << "Register module \"" << self->moduleName << "\" in the runtime!\n";
+    lua_settop(L, 1);
+
+    if (context->registerDeclaration(self) == LuaContext::Code::Error)
+        context->setFailed("Failed to register module " + self->moduleName);
+
+#pragma message("Import needed modules here!")
+
+    LUA_RETURN_NOTHING(L);
+}
+
+void
+ModuleDeclaration::validateModule(lua_State *const L) const noexcept
+{
+    // Minimal module file
+    if (moduleName.empty() || authorName.empty())
+        luaGlobalError(L, "The module does not have the minimal required information");
+
+    // Imports
+    {
+        std::vector<std::string> usedImports{};
+        const auto getUsedImports = [&usedImports](auto vectorOfPtr) noexcept -> void {
+            for (const auto *ptr : vectorOfPtr) {
+                if (!ptr->parentScript.empty())
+                    usedImports.emplace_back(ptr->parentScript);
+            }
+        };
+
+        getUsedImports(moduleJobs);
+        getUsedImports(moduleFunctions);
+
+        Utils::uniqAndSort<std::string>(usedImports);
+        std::vector<std::string> unusedImports =
+            Utils::sortedSetDifference(usedImports, importNames);
+
+        if (!unusedImports.empty()) {
+            std::size_t stringSize = 0;
+            std::string listStrImports =
+                "There are imported modules that are used withour begin declared:";
+            for (const auto &str : unusedImports)
+                stringSize += 1 + str.size();
+            listStrImports.reserve(stringSize);
+            for (const auto &str : unusedImports)
+                listStrImports += " " + str;
+            luaGlobalError(L, listStrImports);
+        }
+    }
+}
diff --git a/src/Lib/Script/CRTPLuaScriptObject/OptionDeclaration.cc b/src/Lib/Script/CRTPLuaScriptObject/OptionDeclaration.cc
new file mode 100644
index 0000000000000000000000000000000000000000..7030865fadffd8f6d7c8a896f6f31d937ce05f47
--- /dev/null
+++ b/src/Lib/Script/CRTPLuaScriptObject/OptionDeclaration.cc
@@ -0,0 +1,50 @@
+#include "../CRTPLuaScriptObject.hh"
+#include "lua.hpp"
+#include <QFileInfo>
+#include <QString>
+
+using namespace Vivy::Script;
+
+int
+OptionDeclaration::TOSTRING(lua_State *const L) noexcept
+{
+    OptionDeclaration *const self = CHECK(L, 1);
+    std::string str(className);
+    str += std::string_view(" { ");
+
+    for (auto const &[name, opt] : self->options) {
+        str += "\"" + name + "\" -> ";
+        str += opt.getSignature();
+        str += ", ";
+    }
+
+    str.resize(str.size() - 2); // Delete the leading ", "
+    str += std::string_view(" }");
+    lua_pushstring(L, str.c_str());
+    return 1;
+}
+
+int
+OptionDeclaration::addOptionContent(lua_State *const L) noexcept
+{
+    OptionDeclaration *const self = CHECK(L, 1);
+    luaL_checktype(L, 2, LUA_TSTRING);
+
+    const std::string_view optName = CHECK_STRING_VIEW(L, 2);
+    const int valueType            = lua_type(L, 3);
+
+    // XXX: Add support for other types!
+    if (valueType == LUA_TNUMBER)
+        self->options.emplace(optName, lua_tonumber(L, 3));
+
+    else if (valueType == LUA_TSTRING)
+        self->options.emplace(optName, CHECK_STRING_VIEW(L, 3));
+
+    else if (valueType == LUA_TBOOLEAN)
+        self->options.emplace(optName, static_cast<bool>(lua_toboolean(L, 3)));
+
+    else
+        luaL_error(L, "Unsupported type %s", luaL_typename(L, 3));
+
+    LUA_RETURN_NOTHING(L);
+}
diff --git a/src/Lib/Script/LuaContext.cc b/src/Lib/Script/LuaContext.cc
index e9e546c6ffe4d964a36a1418c20a278b87c21249..a7c26e659ec30d9e216967d501df263b5c84c285 100644
--- a/src/Lib/Script/LuaContext.cc
+++ b/src/Lib/Script/LuaContext.cc
@@ -10,28 +10,74 @@
 // LuaContext implementation
 using namespace Vivy::Script;
 
-LuaContext::LuaContext() noexcept
+LuaContext::LuaContext(ScriptStore::LoggerType &out, ScriptStore::LoggerType &err) noexcept
     : L(luaL_newstate())
+    , streamOut(out)
+    , streamErr(err)
 {
     luaL_openlibs(L);
-    QFile libVivy(":lua/lib.lua");
-    if (!libVivy.open(QIODevice::ReadOnly | QIODevice::Text))
-        qFatal("Failed to find packaged libVivy (:lia/lib.lua)");
 
-    loadString(libVivy.readAll().toStdString().c_str()); // Load the lib
-    SampleObject::Register(L);                           // Load the C++ objects
+    // Register this context
+    contextList.emplace(L, this);
+
+    // Load all C++ objects
+    FreeFunctions::Register(L);
+    OptionDeclaration::Register(L);
+    JobDeclaration::Register(L);
+    FunctionDeclaration::Register(L);
+    ModuleDeclaration::Register(L);
+
+    // Load the lib
+    loadPackagedFile(":lua/lib.lua");
+    loadPackagedFile(":lua/module.lua");
+}
+
+LuaContext::~LuaContext() noexcept
+{
+    lua_close(L);         // Close lua
+    contextList.erase(L); // Unregister the context
+}
+
+void
+LuaContext::loadPackagedFile(const QString &url) noexcept
+{
+    currentFile = url.toStdString();
+    QFile libVivy(url);
+    if (!libVivy.open(QIODevice::ReadOnly | QIODevice::Text))
+        qFatal("FATA -> failed to find packaged Vivy file %s", url.toStdString().c_str());
+    loadString(libVivy.readAll().toStdString().c_str());
+    currentFile = "";
 }
 
-LuaContext::~LuaContext() noexcept { lua_close(L); }
+LuaContext *
+LuaContext::getContext(const lua_State *const state) noexcept
+{
+    return contextList.contains(state) ? contextList.at(state) : nullptr;
+}
 
 LuaContext::Code
 LuaContext::loadDocument(std::shared_ptr<ScriptDocument> doc) noexcept
 {
-    QFileInfo fileCheck(doc->getName());
-    if (!fileCheck.exists())
+    const QFileInfo fileCheck(doc->getName());
+    if (!fileCheck.exists()) {
+        err(this) << "File " << doc->getName().toStdString() << " for document "
+                  << doc->getUuid().toString().toStdString() << " doesn't exists\n";
         return Code::Error;
+    }
 
-    return loadFile(fileCheck.absoluteFilePath().toStdString().c_str());
+    lastLoadedModule = "";
+    const auto rc    = loadFile(fileCheck.absoluteFilePath().toStdString().c_str());
+    if (rc != Code::Success) {
+        setFailed("Failed to load the module file");
+        return rc;
+    }
+
+    if (lastLoadedModule.empty()) {
+        setFailed("No module was loaded");
+        return Code::Error;
+    }
+
+    return Code::Success;
 }
 
 void
@@ -44,7 +90,7 @@ LuaContext::Code
 LuaContext::exec() noexcept
 {
     if (lua_pcall(L, 0, 0, 0) != LUA_OK) {
-        puts(getLastError().toStdString().c_str());
+        err(this) << getLastLuaError().toStdString().c_str() << "\n";
         return Code::Error;
     }
     return Code::Success;
@@ -54,7 +100,7 @@ LuaContext::Code
 LuaContext::loadString(const char *str) noexcept
 {
     if (luaL_loadstring(L, str) != LUA_OK) {
-        puts("Error loading string");
+        err(this) << "Error loading string: " << getLastLuaError().toStdString() << "\n";
         return Code::Error;
     }
     return exec();
@@ -63,18 +109,173 @@ LuaContext::loadString(const char *str) noexcept
 LuaContext::Code
 LuaContext::loadFile(const char *file) noexcept
 {
+    currentFile = file;
     if (luaL_loadfile(L, file) != LUA_OK) {
-        puts("Error loading file");
+        err(this)
+            << "Error loading file " << file << ": " << getLastLuaError().toStdString() << "\n";
+        currentFile = "";
         return Code::Error;
     }
-    return exec();
+    auto rc     = exec();
+    currentFile = "";
+    return rc;
 }
 
 QString
-LuaContext::getLastError() noexcept
+LuaContext::getLastLuaError() noexcept
 {
+    const int type = lua_type(L, -1);
+    if (type != LUA_TSTRING) {
+        return QStringLiteral("No lua error detected...");
+    }
+
     const char *error = lua_tostring(L, -1);
-    QString ret       = QString("%1").arg(error);
+    QString ret       = QStringLiteral("%1").arg(error);
     lua_pop(L, 1);
     return ret;
 }
+
+lua_State *
+LuaContext::getState() noexcept
+{
+    return L;
+}
+
+bool
+LuaContext::moduleExists(string_view mod) const noexcept
+{
+    return modules.count(mod) >= 1;
+}
+
+LuaContext::Code
+LuaContext::registerDeclaration(const FunctionDeclaration *const fun) noexcept
+{
+    FunctionDeclaration::CHECK(L, -1);
+    return registerToMapTuple(fun->parentScript, fun->name, &functions);
+}
+
+LuaContext::Code
+LuaContext::registerDeclaration(const JobDeclaration *const job) noexcept
+{
+    JobDeclaration::CHECK(L, -1);
+    return registerToMapTuple(job->parentScript, job->name, &jobs);
+}
+
+LuaContext::Code
+LuaContext::registerToMapTuple(
+    string_view module, string_view name,
+    std::map<std::tuple<string_view, string_view>, const int> *const map) noexcept
+{
+    if (map->count({ module, name }) >= 1)
+        return Error;
+
+    streamErr << "Register \"" << module << "::" << name << "\"\n";
+
+    // WARN: The object needs to be already checked!
+    const int r = luaL_ref(L, LUA_REGISTRYINDEX);
+    map->emplace(std::tuple{ module, name }, r);
+    return Success;
+}
+
+LuaContext::Code
+LuaContext::registerDeclaration(const ModuleDeclaration *const mod) noexcept
+{
+    if (moduleExists(mod->moduleName)) {
+        streamErr << "The module \"" << mod->moduleName << "\" is already registered!";
+        return Code::Error;
+    }
+
+    streamErr << "Register module \"" << mod->moduleName << "\"\n";
+
+    ModuleDeclaration::CHECK(L, -1);
+    const int r = luaL_ref(L, LUA_REGISTRYINDEX);
+    modules.emplace(mod->moduleName, r);
+    lastLoadedModule = mod->moduleName;
+    return Code::Success;
+}
+
+ModuleDeclaration *
+LuaContext::getModule(const std::string_view name) const noexcept
+{
+    if (moduleExists(name)) {
+        const int r = modules.at(name);
+        lua_rawgeti(L, LUA_REGISTRYINDEX, r);
+        return ModuleDeclaration::CHECK(L, -1);
+    }
+
+    else {
+        const std::string moduleName(name);
+        luaL_error(L, "Module %s not found", moduleName.c_str());
+        return nullptr;
+    }
+}
+
+FunctionDeclaration *
+LuaContext::getFunction(const std::string_view module, const std::string_view name) const noexcept
+{
+    if (moduleExists(module)) {
+        const int r = functions.at({ module, name });
+        lua_rawgeti(L, LUA_REGISTRYINDEX, r);
+        return FunctionDeclaration::CHECK(L, -1);
+    }
+
+    else {
+        const std::string moduleName(module);
+        const std::string functionName(name);
+        luaL_error(L, "Function %s::%s not found", moduleName.c_str(), functionName.c_str());
+        return nullptr;
+    }
+}
+
+JobDeclaration *
+LuaContext::getJob(const std::string_view module, const std::string_view name) const noexcept
+{
+    if (moduleExists(module) && jobs.count({ module, name }) >= 1) {
+        const int r = jobs.at({ module, name });
+        lua_rawgeti(L, LUA_REGISTRYINDEX, r);
+        return JobDeclaration::CHECK(L, -1);
+    }
+
+    else {
+        const std::string moduleName(module);
+        const std::string jobName(name);
+        luaL_error(L, "Job %s::%s not found", moduleName.c_str(), jobName.c_str());
+        return nullptr; // Unreachable
+    }
+}
+
+const std::vector<std::string_view>
+LuaContext::getAllModuleName() const noexcept
+{
+    std::vector<std::string_view> moduleNames{};
+    for (auto const &[name, _] : modules)
+        moduleNames.emplace_back(name);
+    return moduleNames;
+}
+
+void
+LuaContext::setFailed(const std::string &msg) noexcept
+{
+    failureString = msg;
+    streamErr << "LuaContext is now in failure state: " << msg << "\n";
+}
+
+bool
+LuaContext::isFailed() const noexcept
+{
+    return failureString.size() != 0;
+}
+
+const std::string &
+LuaContext::getFailureDescription() const noexcept
+{
+    return failureString;
+}
+
+const std::string &
+LuaContext::getCurrentLuaFile() const
+{
+    if (currentFile.empty())
+        throw std::logic_error("Not called when a file is being evaluated");
+    return currentFile;
+}
diff --git a/src/Lib/Script/LuaContext.hh b/src/Lib/Script/LuaContext.hh
index 146c0e5f8d6c71bb17c2678be1aece7b37bd6ee9..a852aac6ab4528084e026251bbc6906ccef2b541 100644
--- a/src/Lib/Script/LuaContext.hh
+++ b/src/Lib/Script/LuaContext.hh
@@ -1,5 +1,10 @@
 #pragma once
 
+#include "ScriptStore.hh"
+#include <sstream>
+#include <vector>
+#include <map>
+
 struct lua_State;
 class QString;
 
@@ -8,27 +13,75 @@ namespace Vivy
 class ScriptDocument;
 }
 
+namespace Vivy::Script
+{
+class ModuleDeclaration;
+class FunctionDeclaration;
+class JobDeclaration;
+}
+
 namespace Vivy::Script
 {
 // New Lua script instance
 class LuaContext final {
-    lua_State *L;
+    VIVY_UNMOVABLE_OBJECT(LuaContext)
 
-public:
-    enum class Code {
-        Success,
-        Error,
-    };
+    using string_view = const std::string_view;
+
+    lua_State *L{ nullptr };
+
+    std::string failureString{};
+    std::string currentFile{};
+    std::string lastLoadedModule{};
+
+    std::map<string_view, const int> modules                            = {};
+    std::map<std::tuple<string_view, string_view>, const int> functions = {};
+    std::map<std::tuple<string_view, string_view>, const int> jobs      = {};
+
+    static inline std::map<const lua_State *const, LuaContext *const> contextList = {};
+
+    ScriptStore::LoggerType &streamOut;
+    ScriptStore::LoggerType &streamErr;
 
 public:
-    LuaContext() noexcept;
+    enum class Code { Success, Error };
+    static constexpr inline Code Success = Code::Success;
+    static constexpr inline Code Error   = Code::Error;
+
+    LuaContext(ScriptStore::LoggerType &out, ScriptStore::LoggerType &err) noexcept;
     ~LuaContext() noexcept;
 
     Code loadDocument(std::shared_ptr<ScriptDocument>) noexcept;
+    QString getLastLuaError() noexcept;
+    lua_State *getState() noexcept;
 
-    QString getLastError() noexcept;
+    auto &getOutputStream() noexcept { return streamOut; }
+    auto &getErrorStream() noexcept { return streamErr; }
 
     operator lua_State *() noexcept { return L; }
+    static LuaContext *getContext(const lua_State *const) noexcept;
+
+    // WARN: Need the ModuleDeclaration, the FunctionDeclaration and the
+    // JobDeclaration to be on top of the stack!
+    Code registerDeclaration(const ModuleDeclaration *const) noexcept;
+    Code registerDeclaration(const FunctionDeclaration *const) noexcept;
+    Code registerDeclaration(const JobDeclaration *const) noexcept;
+
+    // Returns the module or `nullptr` if not found.
+    ModuleDeclaration *getModule(const std::string_view) const noexcept;
+    FunctionDeclaration *getFunction(const std::string_view, const std::string_view) const noexcept;
+    JobDeclaration *getJob(const std::string_view, const std::string_view) const noexcept;
+    const std::vector<std::string_view> getAllModuleName() const noexcept;
+
+    // Some verifications gone wrong, this is not a Lua error, this is a script
+    // runtime error.
+    void setFailed(const std::string &) noexcept;
+    bool isFailed() const noexcept;
+    const std::string &getFailureDescription() const noexcept;
+
+    // Makes only sens when called from within loadFile and loadPackagedFile
+    const std::string &getCurrentLuaFile() const;
+    void setLastLoadedModule(const std::string &) noexcept;
 
 private:
     // Exec all loaded strings and files
@@ -36,9 +89,27 @@ private:
 
     Code loadString(const char *) noexcept;
     Code loadFile(const char *) noexcept;
+    void loadPackagedFile(const QString &) noexcept;
+
+    Code registerToMapTuple(
+        const std::string_view module, const std::string_view name,
+        std::map<std::tuple<string_view, string_view>, const int> *const mapPtr) noexcept;
+    bool moduleExists(string_view) const noexcept;
 
     // Swap the env, needed before executing a user script!
     void swapEnv() noexcept;
 };
 
+static inline auto &
+out(LuaContext *const context) noexcept
+{
+    return context->getOutputStream();
+}
+
+static inline auto &
+err(LuaContext *const context) noexcept
+{
+    return context->getErrorStream();
+}
+
 }
diff --git a/src/Lib/Script/ScriptOption.cc b/src/Lib/Script/ScriptOption.cc
new file mode 100644
index 0000000000000000000000000000000000000000..2876d2fc18a14cf3e8560cfada3b0080a76dd2a5
--- /dev/null
+++ b/src/Lib/Script/ScriptOption.cc
@@ -0,0 +1,62 @@
+#include "ScriptOption.hh"
+
+// OptionDeclaration
+using namespace Vivy::Script;
+
+ScriptOption::ScriptOption(const std::string &str) noexcept
+    : string(str)
+    , type(Type::String)
+{
+}
+
+ScriptOption::ScriptOption(const std::string_view str) noexcept
+    : string(str)
+    , type(Type::String)
+{
+}
+
+ScriptOption::ScriptOption(double n) noexcept
+    : number(n)
+    , type(Type::Number)
+{
+}
+
+ScriptOption::ScriptOption(bool b) noexcept
+    : boolean(b)
+    , type(Type::Boolean)
+{
+}
+
+std::string *
+ScriptOption::getString() noexcept
+{
+    return type == Type::String ? &string : nullptr;
+}
+
+double *
+ScriptOption::getNumber() noexcept
+{
+    return type == Type::Number ? &number : nullptr;
+}
+
+bool *
+ScriptOption::getBoolean() noexcept
+{
+    return type == Type::Boolean ? &boolean : nullptr;
+}
+
+std::string_view
+ScriptOption::getSignature() const noexcept
+{
+    static constexpr std::string_view signatureNumber  = "Number";
+    static constexpr std::string_view signatureString  = "String";
+    static constexpr std::string_view signatureBoolean = "Boolean";
+    switch (type) {
+    case Type::String: return signatureString;
+    case Type::Number: return signatureNumber;
+    case Type::Boolean: return signatureBoolean;
+    }
+
+    // Let the program crash
+    qFatal("UNREACHABLE");
+}
diff --git a/src/Lib/Script/ScriptOption.hh b/src/Lib/Script/ScriptOption.hh
new file mode 100644
index 0000000000000000000000000000000000000000..db4076b86fab2f02ca62d72bdecd86525acb8815
--- /dev/null
+++ b/src/Lib/Script/ScriptOption.hh
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <string>
+#include <string_view>
+
+namespace Vivy::Script
+{
+// The options
+class ScriptOption {
+    enum class Type { String, Number, Boolean };
+
+private:
+    std::string string;
+    double number;
+    bool boolean;
+
+    Type type;
+
+public:
+    ScriptOption(const std::string &) noexcept;
+    ScriptOption(const std::string_view) noexcept;
+    ScriptOption(bool) noexcept;
+    ScriptOption(double) noexcept;
+
+    std::string *getString() noexcept;
+    double *getNumber() noexcept;
+    bool *getBoolean() noexcept;
+
+    std::string_view getSignature() const noexcept;
+};
+}
diff --git a/src/Lib/Script/ScriptStore.cc b/src/Lib/Script/ScriptStore.cc
index 12408a70d5ca0cdea7d6e9770d9a0ae8375c273d..61a970d59026592d3b7d10c4e8222f56ba7185f0 100644
--- a/src/Lib/Script/ScriptStore.cc
+++ b/src/Lib/Script/ScriptStore.cc
@@ -1,8 +1,10 @@
 #include "ScriptStore.hh"
 #include "../Uuid.hh"
+#include <iostream>
 
 using namespace Vivy;
 using namespace std::string_literals;
+using Code = Script::LuaContext::Code;
 
 ScriptStore::ScriptStore() noexcept { resetLoadedScripts(); }
 
@@ -12,13 +14,17 @@ ScriptStore::loadScriptFolder(const QString &folderPath)
     QDir folder(folderPath);
     const auto filter = QDir::Files | QDir::NoSymLinks | QDir::Readable;
 
-    // TODO: Sort the list with the dependencies between the scripts
+    // Load all module files, the loadDocument will ask for the implementation
+    // file.
     for (const QString &file : folder.entryList(filter)) {
+        if (!Utils::scriptFileSuffix.contains(QFileInfo(file).suffix()))
+            continue;
+
         auto doc = loadDocument(file);
-        if (luaContext->loadDocument(doc) != Script::LuaContext::Code::Success) {
+        if (luaContext->loadDocument(doc) != Code::Success) {
             const QString error = QStringLiteral("File: ") + file + QStringLiteral(" [") +
                                   doc->getUuid().toString() + QStringLiteral("]") +
-                                  luaContext->getLastError();
+                                  luaContext->getLastLuaError();
             throw std::runtime_error(error.toStdString());
         }
     }
@@ -36,5 +42,45 @@ ScriptStore::getLoadedScripts() const noexcept
 void
 ScriptStore::resetLoadedScripts() noexcept
 {
-    luaContext.reset(new Script::LuaContext());
+    luaContext.reset(new Script::LuaContext(streamOut, streamErr));
+}
+
+std::optional<std::tuple<int, std::string>>
+ScriptStore::executeScript(Uuid id) noexcept
+{
+    const auto script = documents.at(id);
+    const bool ok     = luaContext->loadDocument(script) == Code::Success;
+    QString errorString;
+    std::string buffer;
+
+    std::cout << "STDOUT:\n";
+    while (std::getline(streamOut, buffer, '\n'))
+        std::cout << '\t' << buffer << '\n';
+
+    if (!ok) {
+        // /home/.../sample-spec.module:31: bad argument #1 to 'export'...
+        std::cout << "STDERR:\n";
+        QRegExp errorDetect(QStringLiteral("^([^\\:]*)\\:(\\d+)\\:(.*)\\n?$"));
+        while (std::getline(streamErr, buffer, '\n')) {
+            std::cout << '\t' << buffer << '\n';
+            if (errorDetect.indexIn(buffer.c_str()) != -1) {
+                std::string errorDesc = errorDetect.cap(3).toStdString();
+                return std::tuple{ QString(errorDetect.cap(2)).toInt(), Utils::trim(errorDesc) };
+            }
+        }
+    }
+
+    return std::nullopt;
+}
+
+const std::vector<std::string_view>
+ScriptStore::getLoadedModules() const noexcept
+{
+    return luaContext ? luaContext->getAllModuleName() : std::vector<std::string_view>{};
+}
+
+const Script::ModuleDeclaration *
+ScriptStore::getModule(const std::string_view str) const noexcept
+{
+    return luaContext->getModule(str);
 }
diff --git a/src/Lib/Script/ScriptStore.hh b/src/Lib/Script/ScriptStore.hh
index 88916152d0beb3d42f2f10b8990d5527ede1753b..dd4266a998dc370119156b000c1444d8dbc9ad09 100644
--- a/src/Lib/Script/ScriptStore.hh
+++ b/src/Lib/Script/ScriptStore.hh
@@ -2,22 +2,41 @@
 
 #include "../CRTPStore.hh"
 #include "ScriptDocument.hh"
-#include "LuaContext.hh"
+#include <sstream>
+#include <optional>
+
+namespace Vivy::Script
+{
+class LuaContext;
+class ModuleDeclaration;
+}
 
 namespace Vivy
 {
 class ScriptStore final : public CRTPStore<ScriptStore, ScriptDocument> {
     VIVY_STORAGE_CLASS(ScriptStore, ScriptDocument)
 
-    std::unique_ptr<Script::LuaContext> luaContext{ nullptr };
+    static constexpr inline int outBufferSize = 1024;
 
 public:
-    using Item = std::tuple<Uuid, QString>;
+    using Item       = std::tuple<Uuid, QString>;
+    using LoggerType = std::stringstream;
 
     explicit ScriptStore() noexcept;
 
+    // Load scripts
     void resetLoadedScripts() noexcept;
     void loadScriptFolder(const QString &folderPath);
     std::vector<Item> getLoadedScripts() const noexcept;
+    std::optional<std::tuple<int, std::string>> executeScript(Uuid) noexcept;
+
+    // Get modules from scripts
+    const std::vector<std::string_view> getLoadedModules() const noexcept;
+    const Script::ModuleDeclaration *getModule(const std::string_view) const noexcept;
+
+private:
+    std::unique_ptr<Script::LuaContext> luaContext{ nullptr };
+    LoggerType streamOut;
+    LoggerType streamErr;
 };
 }
diff --git a/src/Lib/Utils.hh b/src/Lib/Utils.hh
index e8095145f4ed85b989f84e08ac03655115757cbf..bf61eedb9d13e2ee85a8d33e90f766b8c4e9b1bc 100644
--- a/src/Lib/Utils.hh
+++ b/src/Lib/Utils.hh
@@ -53,7 +53,7 @@ static const QStringList audioFileSuffix  = { "wave", "wav", "ogg",  "mp3",  "m4
 static const QStringList videoFileSuffix  = { "mkv", "mp4", "mov", "avi", "av1", "m4v", "flv" };
 static const QStringList assFileSuffix    = { "ass" };
 static const QStringList vivyFileSuffix   = { "vivy" };
-static const QStringList scriptFileSuffix = { "lua", "vvs" };
+static const QStringList scriptFileSuffix = { "module" };
 
 const QString &getAudioFileSuffixFilter() noexcept;
 const QString &getVideoFileSuffixFilter() noexcept;
@@ -136,7 +136,7 @@ enum class DocumentType : quint64 {
 
     /* Meta-types */
     Audio = (WAVE | OGG | MP3 | M4A | OPUS | MP2 | AIFF | FLAC | ALAC),
-    Video = (MKV | MP4 | MOV | AVI | AV1 | M4V | FLV),
+    Video = (MKV | MP4 | MOV | AVI | AV1 | M4V | FLV)
 };
 
 template <typename E> constexpr auto
@@ -175,7 +175,7 @@ enum class AudioDocumentType : quint64 {
     MP2  = Utils::toUnderlying(Utils::DocumentType::MP2),
     AIFF = Utils::toUnderlying(Utils::DocumentType::AIFF),
     FLAC = Utils::toUnderlying(Utils::DocumentType::FLAC),
-    ALAC = Utils::toUnderlying(Utils::DocumentType::ALAC),
+    ALAC = Utils::toUnderlying(Utils::DocumentType::ALAC)
 };
 
 // Video document types
@@ -186,13 +186,11 @@ enum class VideoDocumentType : quint64 {
     AVI = Utils::toUnderlying(Utils::DocumentType::AVI),
     AV1 = Utils::toUnderlying(Utils::DocumentType::AV1),
     M4V = Utils::toUnderlying(Utils::DocumentType::M4V),
-    FLV = Utils::toUnderlying(Utils::DocumentType::FLV),
+    FLV = Utils::toUnderlying(Utils::DocumentType::FLV)
 };
 
 // Ass document types
-enum class AssDocumentType : quint64 {
-    ASS = Utils::toUnderlying(Utils::DocumentType::ASS),
-};
+enum class AssDocumentType : quint64 { ASS = Utils::toUnderlying(Utils::DocumentType::ASS) };
 }
 
 class QMenu;
diff --git a/src/Main.cc b/src/Main.cc
index d3a75093f4e098ecd178a9ca9e8e9b2830eab53d..7fb3b3ff18a661594cab1def4edf71ebc839162a 100644
--- a/src/Main.cc
+++ b/src/Main.cc
@@ -1,8 +1,10 @@
 #include "VivyApplication.hh"
+#include "VivyCli.hh"
 #include "Lib/Script/LuaContext.hh"
 
 int
 main(int argc, char **argv) noexcept
 {
-    return Vivy::VivyApplication(argc, argv).exec();
+    return (argc >= 2) ? (Vivy::VivyCli(argc, argv).exec())
+                       : (Vivy::VivyApplication(argc, argv).exec());
 }
diff --git a/src/UI/DocumentViews/MpvContainer.cc b/src/UI/DocumentViews/MpvContainer.cc
index 388e57a01a5a343a19bd7991a46c0b6f4c1f7a65..d3fdd9668dda50999c43bf2f32edd6ab6cc2836d 100644
--- a/src/UI/DocumentViews/MpvContainer.cc
+++ b/src/UI/DocumentViews/MpvContainer.cc
@@ -88,9 +88,9 @@ MpvContainer::handleMpvEvent(mpv_event *event) noexcept
         mpv_event_property *prop;
     };
 
-    auto checkProp = [](mpv_event_property *prop, const std::string &str,
+    auto checkProp = [](mpv_event_property *prop_, const std::string &str,
                         int format) noexcept -> bool {
-        return (prop->name == str) && (prop->format == format);
+        return (prop_->name == str) && (prop_->format == format);
     };
 
     switch (event->event_id) {
diff --git a/src/UI/MainWindow.cc b/src/UI/MainWindow.cc
index 939d3a3cac5e41babefa2ed52c5019c676a9c3ce..b89262cdca7f46e4b925612725a253e312795f72 100644
--- a/src/UI/MainWindow.cc
+++ b/src/UI/MainWindow.cc
@@ -265,8 +265,21 @@ MainWindow::openDocument() noexcept
             (fileType == Utils::DocumentType::Audio) || (fileType == Utils::DocumentType::ASS))
             addTab(new VivyDocumentView(vivyApp->documentStore.loadDocument(filename), documents));
 
-        else if (fileType == Utils::DocumentType::VivyScript)
-            addTab(new ScriptDocumentView(vivyApp->scriptStore.loadDocument(filename), documents));
+        else if (fileType == Utils::DocumentType::VivyScript) {
+            // FIXME: First interpretation of the file to get the error, can do better.
+            std::shared_ptr<ScriptDocument> scriptDocument =
+                vivyApp->scriptStore.loadDocument(filename);
+            std::optional<std::tuple<int, std::string>> errorTuple =
+                vivyApp->scriptStore.executeScript(scriptDocument->getUuid());
+            ScriptDocumentView *newView = new ScriptDocumentView(scriptDocument, documents);
+
+            if (errorTuple.has_value()) {
+                const auto &[line, desc] = errorTuple.value();
+                emit newView->luaErrorFound(line, QString{ desc.c_str() });
+            }
+
+            addTab(newView);
+        }
     } catch (const std::runtime_error &e) {
         qCritical() << "Failed to load document" << filename << "with error:" << e.what();
     }
diff --git a/src/UI/ScriptDocumentView.cc b/src/UI/ScriptDocumentView.cc
index 786dd20365e3d3bc16c4bd16bf71829977d20b84..eea7873654e64e945d79c751c4c5c4673942d2ed 100644
--- a/src/UI/ScriptDocumentView.cc
+++ b/src/UI/ScriptDocumentView.cc
@@ -3,6 +3,7 @@
 #include "ScriptViews/ScriptHighlighter.hh"
 #include "../VivyApplication.hh"
 
+#include <QRegExp>
 #include <QVBoxLayout>
 
 using namespace Vivy;
@@ -21,6 +22,8 @@ ScriptDocumentView::ScriptDocumentView(std::shared_ptr<ScriptDocument> ptr, QWid
     editor->setPlainText(textFile.readAll());
     setCentralWidget(editor);
     editor->setFocus(Qt::OtherFocusReason);
+
+    connect(this, &ScriptDocumentView::luaErrorFound, editor, &ScriptEditor::updateLastLuaError);
 }
 
 void
diff --git a/src/UI/ScriptDocumentView.hh b/src/UI/ScriptDocumentView.hh
index c78c34833d4a2b5d79d9b3657b1fcca745e92632..8e595086bfeae7deba8cf65de2421da827c56eab 100644
--- a/src/UI/ScriptDocumentView.hh
+++ b/src/UI/ScriptDocumentView.hh
@@ -32,10 +32,15 @@ public:
     QIcon getDocumentTabIcon() const noexcept override;
     AbstractDocument *getDocument() const noexcept override;
 
+signals:
+    void luaErrorFound(int, QString);
+
 private:
     ScriptEditor *editor{ nullptr };
     ScriptHighlighter *syntax{ nullptr };
     std::shared_ptr<ScriptDocument> document{ nullptr };
+    QString lastLuaErrorMsg{};
+    int lastLuaErrorLine{ -1 };
 };
 }
 
diff --git a/src/UI/ScriptViews/ScriptEditor.cc b/src/UI/ScriptViews/ScriptEditor.cc
index 362598405bba9536432d40c4373edba5e4ba1d35..7b8a73d94e2231a98f6a4542a6b70fac5d55cabc 100644
--- a/src/UI/ScriptViews/ScriptEditor.cc
+++ b/src/UI/ScriptViews/ScriptEditor.cc
@@ -131,3 +131,9 @@ ScriptEditor::lineNumberAreaPaintEvent(QPaintEvent *event) noexcept
         ++blockNumber;
     }
 }
+
+void
+ScriptEditor::updateLastLuaError(int line, QString desc)
+{
+    qDebug() << "Update error on line" << line << "with description" << desc;
+}
diff --git a/src/UI/ScriptViews/ScriptEditor.hh b/src/UI/ScriptViews/ScriptEditor.hh
index 601a2ed1305420342f0c81f57b35fe9a28c8dc29..01c01769c60d3d324cacb35bb2a55c0684d957d6 100644
--- a/src/UI/ScriptViews/ScriptEditor.hh
+++ b/src/UI/ScriptViews/ScriptEditor.hh
@@ -38,6 +38,9 @@ public:
     void lineNumberAreaPaintEvent(QPaintEvent *event) noexcept;
     int lineNumberAreaWidth() noexcept;
 
+public slots:
+    void updateLastLuaError(int, QString);
+
 protected:
     void resizeEvent(QResizeEvent *event) noexcept override;
     void keyPressEvent(QKeyEvent *e) noexcept override;
diff --git a/src/UI/ScriptViews/ScriptHighlighter.cc b/src/UI/ScriptViews/ScriptHighlighter.cc
index 6112d17646926a3d9c811b90a41ae96dc14277c1..85ea10a8ce763a8bc918fb5928cbbf574f77bda3 100644
--- a/src/UI/ScriptViews/ScriptHighlighter.cc
+++ b/src/UI/ScriptViews/ScriptHighlighter.cc
@@ -21,6 +21,14 @@ ScriptHighlighter::resetHighlightingRule() noexcept
     highlightingRules.emplace_back(QRegExpLiteral("\\b[A-Za-z0-9_]+[ ]*(?=\\()"), functionFormat);
     highlightingRules.emplace_back(QRegExpLiteral("\\b[A-Za-z0-9_]+[ ]*(?=\\{)"), functionFormat);
 
+    // Member and enum members
+    enumFormat.setForeground(fromTheme().enumForeground);
+    enumFormat.setFontWeight(QFont::Bold);
+    memberFormat.setForeground(fromTheme().memberForeground);
+    memberFormat.setFontWeight(QFont::StyleItalic);
+    highlightingRules.emplace_back(QRegExpLiteral("\\.[a-z][\\dA-Za-z]*\\b"), memberFormat);
+    highlightingRules.emplace_back(QRegExpLiteral("\\b[A-Z][\\dA-Z_]*\\b"), enumFormat);
+
     // keywords
     const QStringList keywordPatterns = {
         QStringLiteral("\\bfunction\\b"), QStringLiteral("\\bbreak\\b"),
@@ -32,15 +40,16 @@ ScriptHighlighter::resetHighlightingRule() noexcept
         QStringLiteral("\\bfor\\b"),      QStringLiteral("\\bin\\b"),
         QStringLiteral("\\blocal\\b"),    QStringLiteral("\\bor\\b"),
         QStringLiteral("\\band\\b"),      QStringLiteral("\\bnot\\b"),
-        QStringLiteral("\\breturn\\b"),
+        QStringLiteral("\\breturn\\b"),   QStringLiteral("\\b\\=\\b"),
+        QStringLiteral("\\b\\+\\=\\b"),   QStringLiteral("\\b\\-\\=\\b"),
+        QStringLiteral("\\b\\*\\=\\b"),   QStringLiteral("\\b\\/\\=\\b"),
     };
 
     keywordFormat.setForeground(fromTheme().keywordForeground);
     keywordFormat.setFontWeight(QFont::Bold);
 
-    for (QString const &pattern : keywordPatterns) {
+    for (QString const &pattern : keywordPatterns)
         highlightingRules.emplace_back(QRegExp(pattern), keywordFormat);
-    }
 
     // numbers, boolean, nil
     const QStringList valuePatterns = {
@@ -56,12 +65,11 @@ ScriptHighlighter::resetHighlightingRule() noexcept
     valueFormat.setForeground(fromTheme().valueForeground);
     valueFormat.setFontWeight(QFont::Normal);
 
-    for (QString const &pattern : valuePatterns) {
+    for (QString const &pattern : valuePatterns)
         highlightingRules.emplace_back(QRegExp(pattern), valueFormat);
-    }
 
     // strings
-    quotationFormat.setForeground(fromTheme().quotationForeground);
+    quotationFormat.setForeground(fromTheme().valueForeground);
     highlightingRules.emplace_back(QRegExpLiteral("\"[^\"]*\""), quotationFormat);
     highlightingRules.emplace_back(QRegExpLiteral("\'[^\']*\'"), quotationFormat);
     quoteStartExpression = QRegExpLiteral("\\[\\[");
diff --git a/src/UI/ScriptViews/ScriptHighlighter.hh b/src/UI/ScriptViews/ScriptHighlighter.hh
index 655f8755f96a51b18a9e7e7a9d74f36487eba44b..752c8023455608d13e6177704274d5f9ffd73617 100644
--- a/src/UI/ScriptViews/ScriptHighlighter.hh
+++ b/src/UI/ScriptViews/ScriptHighlighter.hh
@@ -26,8 +26,9 @@ class ScriptHighlighter final : public QSyntaxHighlighter {
         const QBrush functionForeground;
         const QBrush keywordForeground;
         const QBrush valueForeground;
-        const QBrush quotationForeground;
         const QBrush commentForeground;
+        const QBrush memberForeground;
+        const QBrush enumForeground;
     };
 
     enum Theme { DarkTheme = 0, LightTheme = 1, ThemeCount };
@@ -54,19 +55,21 @@ private:
     void resetHighlightingRule() noexcept;
 
     const HighlightingTheme darkHighlightingTheme = {
-        .functionForeground  = Qt::darkCyan,
-        .keywordForeground   = Qt::darkYellow,
-        .valueForeground     = QColor(Qt::cyan).darker(120),
-        .quotationForeground = Qt::darkGreen,
-        .commentForeground   = QColor(Qt::darkGray).darker(120),
+        .functionForeground = Qt::darkCyan,
+        .keywordForeground  = Qt::darkYellow,
+        .valueForeground    = QColor(Qt::cyan).darker(120),
+        .commentForeground  = QColor(Qt::darkGray).darker(120),
+        .memberForeground   = QColor(Qt::darkGray).lighter(160),
+        .enumForeground     = QColor(Qt::magenta).darker(170),
     };
 
     const HighlightingTheme lightHighlightingTheme = {
-        .functionForeground  = Qt::blue,
-        .keywordForeground   = Qt::darkBlue,
-        .valueForeground     = Qt::red,
-        .quotationForeground = Qt::darkGreen,
-        .commentForeground   = QColor(Qt::darkGray).darker(120),
+        .functionForeground = Qt::blue,
+        .keywordForeground  = Qt::darkBlue,
+        .valueForeground    = Qt::red,
+        .commentForeground  = QColor(Qt::darkGray).darker(120),
+        .memberForeground   = QColor(Qt::darkGray).lighter(120),
+        .enumForeground     = Qt::darkMagenta,
     };
 
     const HighlightingTheme highlightingTheme[ThemeCount] = {
@@ -88,5 +91,7 @@ private:
     QTextCharFormat singleLineCommentFormat;
     QTextCharFormat quotationFormat;
     QTextCharFormat functionFormat;
+    QTextCharFormat enumFormat;
+    QTextCharFormat memberFormat;
 };
 }
diff --git a/src/VivyApplication.hh b/src/VivyApplication.hh
index 757dcff18f8e029802675da3cff344b4147512a6..1ba91685512f43b533e4e3cd06a511bb871c35ef 100644
--- a/src/VivyApplication.hh
+++ b/src/VivyApplication.hh
@@ -6,8 +6,8 @@
 
 #define vivyApp static_cast<VivyApplication *>(QApplication::instance())
 
-#define currentVivyDocument() dynamic_cast<VivyDocument*>(vivyApp->getCurrentDocument())
-#define currentScriptDocument dynamic_cast<ScriptDocument*>(vivyApp->getCurrentDocument())
+#define currentVivyDocument() dynamic_cast<VivyDocument *>(vivyApp->getCurrentDocument())
+#define currentScriptDocument dynamic_cast<ScriptDocument *>(vivyApp->getCurrentDocument())
 
 // Only support dark theme for now
 #define VIVY_ICON_APP     ":icons/vivy.png"
diff --git a/src/VivyCli.cc b/src/VivyCli.cc
new file mode 100644
index 0000000000000000000000000000000000000000..1305babcbb026ba9c5984a00aae8529bfcdb2202
--- /dev/null
+++ b/src/VivyCli.cc
@@ -0,0 +1,24 @@
+#include "VivyCli.hh"
+#include <iostream>
+
+using namespace Vivy;
+
+VivyCli::VivyCli(int &argc, char **argv) noexcept
+{
+    if (argc >= 2) {
+        selectedDoc = scriptStore.loadDocument(QString(argv[1]));
+    }
+}
+
+int
+VivyCli::exec() noexcept
+{
+    if ((selectedDoc == nullptr) || (!scriptStore.executeScript(selectedDoc->getUuid())))
+        return 1;
+
+    for (const auto &str : scriptStore.getLoadedModules()) {
+        std::cout << "Module " << str << " was loaded!\n";
+        const auto *mod = scriptStore.getModule(str);
+    }
+    return 0;
+}
diff --git a/src/VivyCli.hh b/src/VivyCli.hh
new file mode 100644
index 0000000000000000000000000000000000000000..9ceef273fa57a52fb9d2e0d2bfbf7eb31aefb037
--- /dev/null
+++ b/src/VivyCli.hh
@@ -0,0 +1,19 @@
+#pragma once
+
+#ifndef __cplusplus
+#error "This is a C++ header"
+#endif
+
+#include "Lib/Script/ScriptStore.hh"
+
+namespace Vivy
+{
+class VivyCli final {
+    ScriptStore scriptStore{};
+    std::shared_ptr<ScriptDocument> selectedDoc{ nullptr };
+
+public:
+    VivyCli(int &argc, char **argv) noexcept;
+    int exec() noexcept;
+};
+}
diff --git a/utils/lua/sample-spec.lua b/utils/lua/sample-spec.lua
deleted file mode 100644
index 999d19418d588b7127656cb975ce8ac9ea458af1..0000000000000000000000000000000000000000
--- a/utils/lua/sample-spec.lua
+++ /dev/null
@@ -1,87 +0,0 @@
---[[
-    Sample script to have a Vivy script specification. Note that all things in
-    a script should be "local" or you won't be saved if you break something
-    else in Vivy...
---]]
-
---[[
-    Get the global variables from the Vivy runtime, like the video dimensions
-    or duration, etc.
---]]
-
-local global = Vivy:global()
-
---[[
-    Get other script, here the util script. You can pass an author in case of
-    ambiguity. The standard scripts are available under the "std" author, thus
-    you can't register scripts in the name of "std". If there is ambiguity, an
-    error will be displayed by Vivy. If the tuple (script, author, revision) is
-    not unique, you will get an error on register time.
---]]
-
-local utils = Vivy:getScript{script = "utils", author = "std"}
-
--- Create a new script.
-local script = Vivy:newScript{
-    name     = "sample_script",
-    author   = "Vivy",
-    revision = "rev-1254",
-    opt      = {
-        option1  = false,
-        preTime  = -900,
-        postTime = 300,
-        color1   = Vivy:newColor("#314159")
-    },
-    description = [[
-        Sample script use for the specifications.
-    ]]
-}
-
--- A local function
-local function tripleCopySyl (syl)
-    return { syl:copy(), syl:copy(), syl:copy() }
-end
-
---[[
-    All functions take as first parameter the opt structure. This is really
-    usefull when calling multiple scripts on the same Vivy document with a
-    document defined options.
---]]
-
-local setRetime = Vivy:newJob {
-    name     = "retime_lines",
-    iterate  = Vivy.Iterable.LINE,
-    callback = function (opt, line)
-        line.start  += opt.preTime
-        line.finish += opt.postTime
-        return line
-    end
-}
-
-local demultiply_syllabes = Vivy:newJob {
-    name     = "create_syllabes",
-    iterate  = Vivy.Iterable.SYLLABE,
-    callback = function (opt, syl)
-        local newSyls = tripleCopySyl(syl)
-
-        newSyls[1].start  = syl.line.start
-        newSyls[1].finish = syl.start
-
-        newSyls[3].finish = syl.line.finish
-        newSyls[3].start  = syl.finish
-
-        -- You can return a table, the runtime will unroll it and add all the
-        -- elements to the new ASS AST.
-        return newSyls
-    end
-}
-
--- Register the callbacks. Will be called in that order
-script:addJobs {
-    set_retime,                        -- Retime lines
-    demultiply_syllabes,               -- Aegisub's syllabe
-    utils:getJob("color_after_read_3") -- 3-patern syls, apply color{1,2}
-}
-
--- Register the script.
-Vivy:registerScript script
diff --git a/utils/lua/sample-spec.module b/utils/lua/sample-spec.module
new file mode 100644
index 0000000000000000000000000000000000000000..b007d97a926900929530b3043f19f42f83e31958
--- /dev/null
+++ b/utils/lua/sample-spec.module
@@ -0,0 +1,54 @@
+--- Declaration
+
+local custom_opt = DeclareOption { option1 = false }
+local time_opt   = DeclareOption { preTime = -900, postTime = 300, }
+local color_opt  = DeclareOption { color1 = Vivy:newColor("#314159") }
+
+local module = DeclareModule {
+    name        = "sample-spec",
+    description = "Sample script used for the specification proposition",
+    author      = "Vivy",
+    revision    = "rev-1254",
+    imports     = { "utils" },
+    options     = { custom_opt, time_opt, color_opt },
+    functions   = {
+        DeclareFunction { "tripleCopySyl" },
+        DeclareFunction { "printFoo" },
+        ImportFunction  { "utils", "prettyPrint" },
+    },
+    jobs = {
+        DeclareJob { "set_retime", options = { time_opt } },
+        DeclareJob { "demultiply_syllabes" },
+        ImportJob  { "utils", "color_after_read_3", "utils", options = { color_opt } }
+    }
+}
+
+--- Implementation
+
+local utils       = module:import { "utils" }
+local prettyPrint = module:import { "utils", "prettyPrint" }
+
+local tripleCopySyl = module:export { "tripleCopySyl", function (syl)
+    return { syl:copy(), syl:copy(), syl:copy() }
+end }
+
+module:export { "printFoo", function () prettyPrint ("Bar") end }
+
+module:export { "retime_lines", LINE, function (opt, line)
+    line.start  = line.start + opt.preTime
+    line.finish = line.start + opt.postTime
+    if line.start  <= Vivy:start()  then line.start  = Vivy:start()  end
+    if line.finish >= Vivy:finish() then line.finish = Vivy:finish() end
+    return line
+end }
+
+module:export { "create_syllabes", SYLLABE, function (opt, syl)
+    local newSyls     = tripleCopySyl(syl)
+    newSyls[1].start  = syl.line.start
+    newSyls[1].finish = syl.start
+    newSyls[3].finish = syl.line.finish
+    newSyls[3].start  = syl.finish
+    return newSyls
+end }
+
+-- vim: ft=lua
diff --git a/utils/lua/simple.module b/utils/lua/simple.module
new file mode 100644
index 0000000000000000000000000000000000000000..85439d803a08c8f063282d4428d5aa0a61577e04
--- /dev/null
+++ b/utils/lua/simple.module
@@ -0,0 +1,48 @@
+--- Declaration
+
+local custom_opt = DeclareOption { option1 = false }
+local time_opt   = DeclareOption { preTime = -900, postTime = 300, }
+local color_opt  = DeclareOption { color1 = Vivy:newColor("#314159") }
+
+local module = DeclareModule {
+    name        = "sample-spec",
+    description = "Sample script used for the specification proposition",
+    author      = "Vivy",
+    revision    = "rev-1254",
+    options     = { custom_opt, time_opt, color_opt },
+    functions   = {
+        DeclareFunction { "tripleCopySyl" },
+        DeclareFunction { "printFoo" },
+    },
+    jobs = {
+        DeclareJob { "set_retime", options = { time_opt } },
+        DeclareJob { "create_syllabes" },
+    }
+}
+
+--- Implementation
+
+local tripleCopySyl = module:export { "tripleCopySyl", function (syl)
+    return { syl:copy(), syl:copy(), syl:copy() }
+end }
+
+module:export { "printFoo", function () prettyPrint ("Bar") end }
+
+module:export { "set_retime", LINE, function (opt, line)
+    line.start  = line.start + opt.preTime
+    line.finish = line.start + opt.postTime
+    if line.start  <= Vivy:start()  then line.start  = Vivy:start()  end
+    if line.finish >= Vivy:finish() then line.finish = Vivy:finish() end
+    return line
+end }
+
+module:export { "create_syllabes", SYLLABE, function (opt, syl)
+    local newSyls     = tripleCopySyl(syl)
+    newSyls[1].start  = syl.line.start
+    newSyls[1].finish = syl.start
+    newSyls[3].finish = syl.line.finish
+    newSyls[3].start  = syl.finish
+    return newSyls
+end }
+
+-- vim: ft=lua