Creating a Component

A Component is defined once with Component.new(), then watched from a Service's OwlStart. From that point on, Owl handles creating and destroying instances entirely on its own you only ever write the lifecycle and the custom methods.

Defining a Component

ServerScriptService/Services/EnemyService.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Owl = require(ReplicatedStorage.OwlKnit.Owl)
local Component = require(Owl.OwlComponent)
 
-- 1. Define the Component type
local EnemyComponent = Component.new({
    Tag = "Enemy", -- > // CollectionService tag
    Type = "Server", -- > // "Server" | "Client" | "Shared"
    Ancestors = { workspace }, -- > // ancestor whitelist (empty = accept everything)
})
 
-- 2. Define the lifecycle
 
function EnemyComponent:Construct()
    -- > // Called when the tagged instance is created
    -- > // Read Attributes, set up variables
    self.Health = self.Instance:GetAttribute("MaxHealth") or 100
    self.MaxHealth = self.Health
    self.IsDead = false
end
 
function EnemyComponent:Start()
    -- > // Called right after Construct, connect events here
    -- > // self._trove is available and cleaned up automatically on destruction
    self._trove:Add(self.Instance.Destroying:Connect(function()
        print(self.Instance.Name, "destroyed.")
    end))
end
 
function EnemyComponent:Destroy()
    -- > // Custom cleanup (the _trove is destroyed automatically right after this runs)
    print(self.Instance.Name, "component cleaned up.")
end
 
-- 3. Custom methods
 
function EnemyComponent:TakeDamage(amount: number)
    if self.IsDead then return end
 
    self.Health = math.max(0, self.Health - amount)
 
    if self.Health <= 0 then
        self.IsDead = true
        self.Instance:Destroy()
    end
end
 
function EnemyComponent:GetHealthPercent(): number
    return self.Health / self.MaxHealth
end
 
-- 4. Start watching, from the Service
 
local EnemyService = Owl.CreateService({ Name = "EnemyService" })
 
function EnemyService:OwlStart()
    -- > // Watch() starts the CollectionService monitoring.
    -- > // Owl.Start() also calls it automatically if Type matches the current context.
    EnemyComponent:Watch()
end
 
return EnemyService

Construct runs once, immediately when the tag is added this is where you read Attributes off the instance and initialize any state the rest of the Component needs. Start runs right after and is where you connect events; anything added to self._trove here is disconnected automatically the moment the Component is destroyed, so you rarely need to track connections manually. Destroy runs for any extraa cleanup beyond what the Trove already handles logging or side effects outside the instance itself.

Accessing a Component from elsewhere

-- > // From DamageService
local EnemyComponent = require(path.to.EnemyComponent)
 
-- > // Direct access to one instance's Component
local comp = EnemyComponent:Get(someInstance)
if comp then
    comp:TakeDamage(25)
end
 
-- > // Every currently active Component of this type
for _, enemy in ipairs(EnemyComponent:GetAll()) do
    enemy:TakeDamage(10)
end
 
-- > // Wait for a Component to be ready (Promise)
EnemyComponent:WaitFor(someInstance, 5)
    :andThen(function(comp)
        comp:TakeDamage(50)
    end)
    :catch(warn)

:Get(instance) returns nil if the instance isn't tagged (or its Component hasn't finished Construct yet) reach for :WaitFor(instance, timeout) instead when you know the tag was just added and there's a real chance you're asking before Construct has had a chance to run, rather than manually retrying :Get in a loop.

Where to go next

  • Extensions - sharing Construct/Start/Destroy behavior across multiple Component types.
  • API Reference - the full signature for Component.new, Added, Removed and every method shown above.