diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,19 @@
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## 0.8.0.0 - 2023-03-06
+
+### Changed
+
+ - `SourceHostingSite` was renamed to `Forge`
+
+### Added
+
+ - GitLab endpoints
+ - `onSchedule` information
+ - Various small additions
+
+
 ## 0.7.2.1 - 2022-12-29
 
 Maintenance and tooling update.
diff --git a/Setup.hs b/Setup.hs
deleted file mode 100644
--- a/Setup.hs
+++ /dev/null
@@ -1,3 +0,0 @@
-import Distribution.Simple
-
-main = defaultMain
diff --git a/hercules-ci-api.cabal b/hercules-ci-api.cabal
--- a/hercules-ci-api.cabal
+++ b/hercules-ci-api.cabal
@@ -1,6 +1,6 @@
-cabal-version:      1.12
+cabal-version:      2.2
 name:               hercules-ci-api
-version:            0.7.2.1
+version:            0.8.0.0
 synopsis:           Hercules CI API definition with Servant
 homepage:           https://github.com/hercules-ci/hercules-ci-agent#readme
 bug-reports:        https://github.com/hercules-ci/hercules-ci-agent/issues
@@ -15,7 +15,32 @@
   type:     git
   location: https://github.com/hercules-ci/hercules-ci
 
+common defaults
+  default-language:   Haskell2010
+  default-extensions:
+    NoImplicitPrelude
+    DeriveGeneric
+    DeriveTraversable
+    DisambiguateRecordFields
+    FlexibleContexts
+    InstanceSigs
+    LambdaCase
+    MultiParamTypeClasses
+    OverloadedStrings
+    RankNTypes
+    TupleSections
+    TypeApplications
+    TypeOperators
+
+  ghc-options:
+    -Wall -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures
+    -fwarn-name-shadowing -fwarn-incomplete-patterns
+
+common defaults-exe
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+
 library
+  import:          defaults
   exposed-modules:
     Hercules.API
     Hercules.API.Accounts
@@ -53,12 +78,24 @@
     Hercules.API.Build.FailureGraph
     Hercules.API.Build.Log
     Hercules.API.Build.LogLine
+    Hercules.API.ClientInfo
+    Hercules.API.ClientInfo.ClientInfo
     Hercules.API.Derivation
     Hercules.API.Effects
     Hercules.API.Effects.EffectEvent
     Hercules.API.Effects.EffectInfo
+    Hercules.API.Effects.EffectReference
+    Hercules.API.Error
     Hercules.API.Evaluation.AttributeError
     Hercules.API.Evaluation.Evaluation
+    Hercules.API.Forge
+    Hercules.API.Forge.Forge
+    Hercules.API.Forge.SimpleForge
+    Hercules.API.GitLab
+    Hercules.API.GitLab.CreateInstallationBuilderRequest
+    Hercules.API.GitLab.CreateInstallationBuilderResponse
+    Hercules.API.GitLab.InstallationBuilder
+    Hercules.API.GitLab.PatchInstallationBuilder
     Hercules.API.Health
     Hercules.API.Inputs.ImmutableGitInput
     Hercules.API.Inputs.ImmutableInput
@@ -75,6 +112,10 @@
     Hercules.API.Projects.CreateProject
     Hercules.API.Projects.CreateUserEffectTokenResponse
     Hercules.API.Projects.Job
+    Hercules.API.Projects.JobHandlers
+    Hercules.API.Projects.JobHandlers.OnPushHandler
+    Hercules.API.Projects.JobHandlers.OnScheduleHandler
+    Hercules.API.Projects.LegacySimpleJob
     Hercules.API.Projects.PatchProject
     Hercules.API.Projects.Project
     Hercules.API.Projects.SimpleJob
@@ -85,8 +126,6 @@
     Hercules.API.Repos.SimpleRepo
     Hercules.API.Result
     Hercules.API.Servant.Status
-    Hercules.API.SourceHostingSite.SimpleSite
-    Hercules.API.SourceHostingSite.SourceHostingSite
     Hercules.API.State
     Hercules.API.State.ProjectState
     Hercules.API.State.StateFile
@@ -97,28 +136,9 @@
     Hercules.API.State.StateVersion
     Hercules.Frontend
 
-  hs-source-dirs:     src
-  default-extensions:
-    NoImplicitPrelude
-    DeriveGeneric
-    DeriveTraversable
-    DisambiguateRecordFields
-    FlexibleContexts
-    InstanceSigs
-    LambdaCase
-    MultiParamTypeClasses
-    OverloadedStrings
-    RankNTypes
-    TupleSections
-    TypeApplications
-    TypeOperators
-
-  ghc-options:
-    -Wall -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures
-    -fwarn-name-shadowing -fwarn-incomplete-patterns
-
+  hs-source-dirs:  src
   build-depends:
-      aeson >= 2
+    , aeson                    >=2
     , base                     >=4.7    && <5
     , bytestring
     , containers
@@ -144,33 +164,12 @@
     , time
     , uuid
 
-  default-language:   Haskell2010
-
 executable hercules-gen-swagger
-  main-is:            Main.hs
-  hs-source-dirs:     hercules-gen-swagger
-  default-extensions:
-    NoImplicitPrelude
-    DeriveGeneric
-    DeriveTraversable
-    DisambiguateRecordFields
-    FlexibleContexts
-    InstanceSigs
-    LambdaCase
-    MultiParamTypeClasses
-    OverloadedStrings
-    RankNTypes
-    TupleSections
-    TypeApplications
-    TypeOperators
-
-  ghc-options:
-    -Wall -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures
-    -fwarn-name-shadowing -fwarn-incomplete-patterns -threaded -rtsopts
-    -with-rtsopts=-N
-
+  import:         defaults, defaults-exe
+  main-is:        Main.hs
+  hs-source-dirs: hercules-gen-swagger
   build-depends:
-      aeson
+    , aeson
     , base                     >=4.7    && <5
     , bytestring
     , containers
@@ -195,4 +194,36 @@
     , time
     , uuid
 
-  default-language:   Haskell2010
+test-suite hercules-ci-api-unit-tests
+  import:           defaults, defaults-exe
+  type:             exitcode-stdio-1.0
+  main-is:          Main.hs
+  other-modules:
+    Hercules.API.AttributeSpec
+    Spec
+
+  hs-source-dirs:   test
+  build-depends:
+    , aeson
+    , base
+    , bytestring
+    , containers
+    , exceptions
+    , hashable
+    , hercules-ci-api
+    , hercules-ci-api-core
+    , hspec
+    , http-api-data
+    , http-media
+    , protolude
+    , QuickCheck
+    , quickcheck-classes
+    , servant               >=0.14.1
+    , servant-auth
+    , string-conv
+    , text
+    , time
+    , uuid
+    , vector
+
+  default-language: Haskell2010
diff --git a/src/Hercules/API.hs b/src/Hercules/API.hs
--- a/src/Hercules/API.hs
+++ b/src/Hercules/API.hs
@@ -1,6 +1,7 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE PolyKinds #-}
 {-# LANGUAGE ScopedTypeVariables #-}
+{-# OPTIONS_GHC -O0 #-}
 
 module Hercules.API
   ( api,
@@ -36,7 +37,10 @@
 import Hercules.API.Build as Client
   ( BuildAPI,
   )
+import Hercules.API.ClientInfo (ClientInfoAPI)
 import Hercules.API.Effects (EffectsAPI)
+import Hercules.API.Forge (ForgeAPI)
+import Hercules.API.GitLab (GitLabAPI)
 import Hercules.API.Health (HealthAPI)
 import Hercules.API.Organizations (OrganizationsAPI)
 import Hercules.API.Orphans ()
@@ -55,6 +59,8 @@
 -- TODO remove health so we get clientapi
 data HerculesAPI auth f = HerculesAPI
   { accounts :: f :- ToServantApi (AccountsAPI auth),
+    clientInfo :: f :- ToServantApi (ClientInfoAPI auth),
+    forges :: f :- ToServantApi (ForgeAPI auth),
     repos :: f :- ToServantApi (ReposAPI auth),
     projects :: f :- ToServantApi (ProjectsAPI auth),
     agents :: f :- ToServantApi (AgentsAPI auth),
@@ -62,19 +68,23 @@
     effects :: f :- ToServantApi (EffectsAPI auth),
     health :: f :- ToServantApi (HealthAPI auth),
     organizations :: f :- ToServantApi (OrganizationsAPI auth),
-    state :: f :- ToServantApi (StateAPI auth)
+    state :: f :- ToServantApi (StateAPI auth),
+    gitlab :: f :- ToServantApi (GitLabAPI auth)
   }
   deriving (Generic)
 
 data ClientAPI auth f = ClientAPI
   { clientAccounts :: f :- ToServantApi (AccountsAPI auth),
+    clientClientInfo :: f :- ToServantApi (ClientInfoAPI auth),
+    clientForges :: f :- ToServantApi (ForgeAPI auth),
     clientRepos :: f :- ToServantApi (ReposAPI auth),
     clientProjects :: f :- ToServantApi (ProjectsAPI auth),
     clientAgents :: f :- ToServantApi (AgentsAPI auth),
     clientBuild :: f :- ToServantApi (Client.BuildAPI auth),
     clientEffects :: f :- ToServantApi (EffectsAPI auth),
     clientOrganizations :: f :- ToServantApi (OrganizationsAPI auth),
-    clientState :: f :- ToServantApi (StateAPI auth)
+    clientState :: f :- ToServantApi (StateAPI auth),
+    clientGitLab :: f :- ToServantApi (GitLabAPI auth)
   }
   deriving (Generic)
 
@@ -102,7 +112,7 @@
 
 swagger :: Swagger
 swagger =
-  toSwagger (servantClientApi @(Auth '[JWT] ()))
+  toSwagger api'
     & info
       . title
       .~ "Hercules CI API"
@@ -112,6 +122,23 @@
     & info
       . description
       ?~ "You have reached the Hercules Continuous Integration Application Programming Interface. This user interface provides human friendly access to the various endpoints. To get started with Hercules CI, see hercules-ci.com. Happy building! —the Hercules team"
+    & withTags clientProjects "project" "Project and job operations"
+    & withTags clientBuild "build" "Build related operations"
+    & withTags clientEffects "effect" "Effect related operations"
+    & withTags clientState "state" "State files and locks, commonly used with effects and the hci command"
+    & withTags clientForges "forge" "Forge operations"
+    & withTags clientGitLab "gitlab" "GitLab-specific operations"
+    & withTags clientRepos "repo" "Repository operations"
+    & withTags clientAgents "agent" "Agent admin and monitoring operations"
+    & withTags clientAccounts "account" "Account operations"
+    & withTags clientOrganizations "organization" "Organizations and billing operations"
+    & withTags clientClientInfo "client" "Ad hoc endpoints for the frontend and perhaps some client-side use cases"
+  where
+    api' = (servantClientApi @(Auth '[JWT] ()))
+    withTags f tag desc = applyTagsFor (subOperations (clientApiProxy f) api') [tag & description ?~ desc]
+
+clientApiProxy :: (ClientAPI (Auth '[JWT] ()) AsApi -> a) -> Proxy ("api" :> "v1" :> a)
+clientApiProxy _ = Proxy
 
 -- | 'Control.Monad.void' specialised to 'NoContent' to soothe the
 -- compiler that rightfully warns about throwing away a do notation
diff --git a/src/Hercules/API/Accounts.hs b/src/Hercules/API/Accounts.hs
--- a/src/Hercules/API/Accounts.hs
+++ b/src/Hercules/API/Accounts.hs
@@ -15,10 +15,8 @@
 import Hercules.API.Accounts.CLITokensResponse (CLITokensResponse)
 import Hercules.API.Accounts.NotificationSettings (NotificationSettings)
 import Hercules.API.Accounts.NotificationSettingsPatch (NotificationSettingsPatch)
+import Hercules.API.Forge.Forge (Forge)
 import Hercules.API.Prelude hiding (id)
-import Hercules.API.SourceHostingSite.SourceHostingSite
-  ( SourceHostingSite,
-  )
 import Servant.API
 import Servant.Auth ()
 
@@ -86,7 +84,7 @@
       f
         :- Substitute
              ( "site"
-                 :> Capture "site" (Name SourceHostingSite)
+                 :> Capture "site" (Name Forge)
                  :> "account"
                  :> Capture "account" (Name Account)
                  :> Placeholder
@@ -96,7 +94,7 @@
       f
         :- Summary "Accounts that the authenticated user owns, admins or collaborates with."
           :> "accounts"
-          :> QueryParam "site" (Name SourceHostingSite)
+          :> QueryParam "site" (Name Forge)
           :> QueryParam "name" (Name Account)
           :> auth
           :> Get '[JSON] [Account],
@@ -162,7 +160,7 @@
       f
         :- Summary "Retrieve installation status after redirect from external source site settings."
           :> "sites"
-          :> Capture "siteId" (Id SourceHostingSite)
+          :> Capture "forgeId" (Id Forge)
           :> "installation"
           :> Capture "installationId" Int
           :> "status"
diff --git a/src/Hercules/API/Accounts/Account.hs b/src/Hercules/API/Accounts/Account.hs
--- a/src/Hercules/API/Accounts/Account.hs
+++ b/src/Hercules/API/Accounts/Account.hs
@@ -2,9 +2,9 @@
 
 module Hercules.API.Accounts.Account where
 
+import Hercules.API.Forge.Forge (Forge)
 import qualified Hercules.API.Organizations.Organization as Organization
 import Hercules.API.Prelude
-import Hercules.API.SourceHostingSite.SourceHostingSite (SourceHostingSite)
 
 data AccountType = User | Organization
   deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
@@ -35,7 +35,7 @@
     -- Hercules CI on this account.
     isInstallable :: Bool,
     membershipRole :: Maybe MembershipRole,
-    sourceHostingSites :: Maybe (Map Text SourceHostingSite),
+    sourceHostingSites :: Maybe (Map Text Forge),
     manageInstallationURL :: Maybe Text,
     installationIsSelection :: Maybe Bool
   }
diff --git a/src/Hercules/API/Accounts/AccountInstallationStatus.hs b/src/Hercules/API/Accounts/AccountInstallationStatus.hs
--- a/src/Hercules/API/Accounts/AccountInstallationStatus.hs
+++ b/src/Hercules/API/Accounts/AccountInstallationStatus.hs
@@ -3,11 +3,11 @@
 module Hercules.API.Accounts.AccountInstallationStatus where
 
 import Hercules.API.Accounts.Account (Account)
+import Hercules.API.Forge.Forge (Forge)
 import Hercules.API.Prelude
-import Hercules.API.SourceHostingSite.SourceHostingSite (SourceHostingSite)
 
 data AccountInstallationStatus = AccountInstallationStatus
-  { site :: SourceHostingSite,
+  { site :: Forge,
     account :: Maybe Account,
     isProcessingInstallationWebHook :: Bool,
     secondsSinceInstallationWebHookComplete :: Maybe Int
diff --git a/src/Hercules/API/Accounts/NotificationSettings.hs b/src/Hercules/API/Accounts/NotificationSettings.hs
--- a/src/Hercules/API/Accounts/NotificationSettings.hs
+++ b/src/Hercules/API/Accounts/NotificationSettings.hs
@@ -4,8 +4,8 @@
 module Hercules.API.Accounts.NotificationSettings where
 
 import Hercules.API.Accounts.SimpleAccount (SimpleAccount)
+import Hercules.API.Forge.SimpleForge (SimpleForge)
 import Hercules.API.Prelude
-import Hercules.API.SourceHostingSite.SimpleSite (SimpleSite)
 
 data NotificationLevel
   = Ignore
@@ -27,7 +27,7 @@
 data AuthorizedEmail = AuthorizedEmail
   { address :: Text,
     isPrimary :: Bool,
-    source :: Maybe SimpleSite
+    source :: Maybe SimpleForge
   }
   deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
 
diff --git a/src/Hercules/API/Accounts/SimpleAccount.hs b/src/Hercules/API/Accounts/SimpleAccount.hs
--- a/src/Hercules/API/Accounts/SimpleAccount.hs
+++ b/src/Hercules/API/Accounts/SimpleAccount.hs
@@ -3,8 +3,8 @@
 module Hercules.API.Accounts.SimpleAccount where
 
 import Hercules.API.Accounts.Account (Account, AccountType)
+import Hercules.API.Forge.SimpleForge (SimpleForge)
 import Hercules.API.Prelude
-import Hercules.API.SourceHostingSite.SimpleSite (SimpleSite)
 
 data SimpleAccount = SimpleAccount
   { id :: Id Account,
@@ -12,6 +12,6 @@
     displayName :: Text,
     typ :: AccountType,
     imageURL :: Text,
-    site :: SimpleSite
+    site :: SimpleForge
   }
   deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/Agents/AgentSession.hs b/src/Hercules/API/Agents/AgentSession.hs
--- a/src/Hercules/API/Agents/AgentSession.hs
+++ b/src/Hercules/API/Agents/AgentSession.hs
@@ -19,10 +19,12 @@
     platforms :: [Text],
     systemFeatures :: [Text],
     cachixPushCaches :: [Text],
+    pushCaches :: [Text],
     substituters :: [Text],
     creation :: UTCTime,
     lastSeen :: Maybe UTCTime,
     available :: Bool,
+    concurrentTasks :: Int,
     labels :: Labels
   }
   deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/Attribute.hs b/src/Hercules/API/Attribute.hs
--- a/src/Hercules/API/Attribute.hs
+++ b/src/Hercules/API/Attribute.hs
@@ -1,12 +1,20 @@
 {-# LANGUAGE DeriveAnyClass #-}
 {-# LANGUAGE StandaloneDeriving #-}
 
-module Hercules.API.Attribute where
+module Hercules.API.Attribute
+  ( AttributeType (..),
+    Attribute (..),
+    AttributePath (..),
+    attributePathFromString,
+    attributePathToString,
+  )
+where
 
 import Control.Applicative (Alternative ((<|>)))
 import Control.Lens (at, (%~))
 import qualified Data.Aeson as A
 import Data.Aeson.Lens
+import Data.Function ((&))
 import Data.Proxy (Proxy (Proxy))
 import Data.Swagger (ToParamSchema (..))
 import qualified Data.Text as T
@@ -50,8 +58,7 @@
   deriving (Generic, Eq, NFData)
 
 instance FromHttpApiData AttributePath where
-  -- TODO parse properly
-  parseUrlPiece = Right . AttributePath . T.split (== '.')
+  parseUrlPiece = Right . AttributePath . attributePathFromString
 
   parseQueryParam = parseUrlPiece
 
@@ -59,4 +66,64 @@
   toParamSchema _ = toParamSchema (Proxy :: Proxy Text)
 
 instance ToHttpApiData AttributePath where
-  toUrlPiece = toUrlPiece . T.intercalate "." . fromAttributePath
+  toUrlPiece = toUrlPiece . attributePathToString . fromAttributePath
+
+----------------------------------------
+
+attributePathToString :: [Text] -> Text
+attributePathToString l =
+  l & map stringToNixIdentifier & T.intercalate "."
+
+stringToNixIdentifier :: Text -> Text
+stringToNixIdentifier s =
+  if isNixSimpleId s
+    then s
+    else nixQuote s
+
+isNixSimpleId :: Text -> Bool
+isNixSimpleId t | T.null t = False
+isNixSimpleId t
+  | not
+      ( let h = T.head t
+         in (h >= 'a' && h <= 'z')
+              || (h >= 'A' && h <= 'Z')
+              || h == '_'
+      ) =
+      False
+isNixSimpleId t =
+  T.all
+    ( \c ->
+        (c >= 'a' && c <= 'z')
+          || (c >= 'A' && c <= 'Z')
+          || (c >= '0' && c <= '9')
+          || c == '_'
+          || c == '\''
+          || c == '-'
+    )
+    t
+
+nixQuote :: Text -> Text
+nixQuote s =
+  "\""
+    <> T.replace "\"" "\\\"" (T.replace "\\" "\\\\" s)
+    <> "\""
+
+----------------------------------------
+
+attributePathFromString :: Text -> [Text]
+attributePathFromString "" = []
+attributePathFromString t = t & T.unpack & attributePathFromStringImpl [] "" & reverse & fmap T.pack
+
+attributePathFromStringImpl :: [String] -> String -> [Char] -> [String]
+attributePathFromStringImpl = \accPath accAttr chars -> case chars of
+  [] -> reverse accAttr : accPath
+  ('.' : cs) -> attributePathFromStringImpl (reverse accAttr : accPath) "" cs
+  ('\"' : cs) -> attributePathFromStringElement accPath "" cs
+  (c : cs) -> attributePathFromStringImpl accPath (c : accAttr) cs
+
+attributePathFromStringElement :: [String] -> String -> [Char] -> [String]
+attributePathFromStringElement = \accPath accAttr chars -> case chars of
+  ('"' : cs) -> attributePathFromStringImpl accPath accAttr cs
+  ('\\' : c : cs) -> attributePathFromStringElement (accPath) (c : accAttr) cs
+  [] -> reverse accAttr : accPath
+  (c : cs) -> attributePathFromStringElement accPath (c : accAttr) cs
diff --git a/src/Hercules/API/Auth.hs b/src/Hercules/API/Auth.hs
--- a/src/Hercules/API/Auth.hs
+++ b/src/Hercules/API/Auth.hs
@@ -2,11 +2,22 @@
 
 module Hercules.API.Auth where
 
+import Hercules.API.Forge.Forge (Forge)
 import Hercules.API.Prelude
 import Hercules.API.Servant.Status
 import Servant.API
 import Web.Cookie (SetCookie)
 
+type OAuthReturn stage view =
+  "api"
+    :> "auth"
+    :> "forge"
+    :> Capture "forge" (Id Forge)
+    :> "return"
+    :> QueryParam' '[stage, Strict] "code" Text
+    :> QueryParam' '[Optional, Strict] "redirect" Text
+    :> view
+
 -- | Endpoints for authentication
 data AuthAPI auth f = AuthAPI
   { initiateGitHubLogin ::
@@ -18,10 +29,38 @@
           :> Get302 '[PlainText, JSON] '[],
     signOut ::
       f
-        :- "api"
+        :- Summary "Terminate the session and help clear the cookies."
+          :> "api"
           :> "auth"
           :> "sign-out"
           :> auth
-          :> Post '[JSON] (Headers '[Header "Set-Cookie" SetCookie, Header "Set-Cookie" SetCookie] NoContent)
+          :> Post '[JSON] (Headers '[Header "Set-Cookie" SetCookie, Header "Set-Cookie" SetCookie] NoContent),
+    start ::
+      f
+        :- Summary "Initiate an OAuth login, redirecting the client to a URL at the OAuth provider."
+          :> "api"
+          :> "auth"
+          :> Capture "type" Text
+          :> "start"
+          :> Capture "forgeId" (Id Forge)
+          :> QueryParam' '[Optional, Strict] "redirect" Text
+          :> auth
+          :> Get302 '[PlainText, JSON] '[],
+    return ::
+      f
+        :- OAuthReturn
+             Required
+             ( auth
+                 :> Get302
+                      '[PlainText, JSON]
+                      '[ Header "Set-Cookie" SetCookie,
+                         Header "Set-Cookie" SetCookie
+                       ]
+             )
+  }
+  deriving (Generic)
+
+data AuthRoutes view f = AuthRoutes
+  { authRouteReturn :: f :- OAuthReturn Optional view
   }
   deriving (Generic)
diff --git a/src/Hercules/API/BillingStatus.hs b/src/Hercules/API/BillingStatus.hs
--- a/src/Hercules/API/BillingStatus.hs
+++ b/src/Hercules/API/BillingStatus.hs
@@ -16,6 +16,7 @@
   | Active
   | Cancelled
   | External
+  | Enterprise
   deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
 
 toText :: BillingStatus -> Text
@@ -24,6 +25,7 @@
 toText Active = "Active"
 toText Cancelled = "Cancelled"
 toText External = "External"
+toText Enterprise = "Enterprise"
 
 fromText :: Text -> Maybe BillingStatus
 fromText "Community" = Just Community
@@ -31,4 +33,5 @@
 fromText "Trial" = Just Trial
 fromText "Active" = Just Active
 fromText "External" = Just External
+fromText "Enterprise" = Just Enterprise
 fromText _ = Nothing
diff --git a/src/Hercules/API/Build.hs b/src/Hercules/API/Build.hs
--- a/src/Hercules/API/Build.hs
+++ b/src/Hercules/API/Build.hs
@@ -20,6 +20,17 @@
           :> "retry"
           :> auth
           :> Post '[PlainText, JSON] NoContent,
+    cancelDerivation ::
+      f
+        :- Summary "Cancel a derivation"
+          :> Description "If running, the build or push process will be killed. It will not be restarted, unless a rebuild is requested, or when output contents are required during evaluation (import from derivation)."
+          :> "accounts"
+          :> Capture "accountId" (Id Account)
+          :> "derivations"
+          :> Capture "derivationPath" Text
+          :> "cancel"
+          :> auth
+          :> Post '[PlainText, JSON] NoContent,
     readDerivationLogText ::
       f
         :- Summary "Read a derivation build log"
diff --git a/src/Hercules/API/Build/DerivationEvent.hs b/src/Hercules/API/Build/DerivationEvent.hs
--- a/src/Hercules/API/Build/DerivationEvent.hs
+++ b/src/Hercules/API/Build/DerivationEvent.hs
@@ -5,6 +5,7 @@
 module Hercules.API.Build.DerivationEvent where
 
 import Data.Aeson.Types (FromJSON (..), ToJSON (..), genericParseJSON, genericToEncoding, genericToJSON)
+import Hercules.API.Accounts.SimpleAccount (SimpleAccount)
 import Hercules.API.Build.DerivationEvent.BuiltOutput
 import Hercules.API.Prelude
 import Hercules.API.Projects.SimpleJob (SimpleJob)
@@ -17,6 +18,7 @@
   | Failed DerivationEventFailed
   | Succeeded DerivationEventSucceeded
   | Cancelled DerivationEventCancelled
+  | ForceCancelled DerivationEventForceCancelled
   | Built DerivationEventBuilt
   | HasCancelled DerivationEventHasCancelled
   | HasCancelledForReset DerivationEventHasCancelledForReset
@@ -31,16 +33,17 @@
   toEncoding = genericToEncoding schemaCompatibleOptions
 
 eventTime :: DerivationEvent -> UTCTime
-eventTime (Queued DerivationEventQueued {time = t}) = t
-eventTime (DependencyFailed DerivationEventDependencyFailed {time = t}) = t
-eventTime (Started DerivationEventStarted {time = t}) = t
-eventTime (Reset DerivationEventReset {time = t}) = t
-eventTime (Failed DerivationEventFailed {time = t}) = t
-eventTime (Succeeded DerivationEventSucceeded {time = t}) = t
-eventTime (Cancelled DerivationEventCancelled {time = t}) = t
-eventTime (Built DerivationEventBuilt {time = t}) = t
-eventTime (HasCancelled DerivationEventHasCancelled {time = t}) = t
-eventTime (HasCancelledForReset DerivationEventHasCancelledForReset {time = t}) = t
+eventTime (Queued (DerivationEventQueued {time = t})) = t
+eventTime (DependencyFailed (DerivationEventDependencyFailed {time = t})) = t
+eventTime (Started (DerivationEventStarted {time = t})) = t
+eventTime (Reset (DerivationEventReset {time = t})) = t
+eventTime (Failed (DerivationEventFailed {time = t})) = t
+eventTime (Succeeded (DerivationEventSucceeded {time = t})) = t
+eventTime (Cancelled (DerivationEventCancelled {time = t})) = t
+eventTime (ForceCancelled (DerivationEventForceCancelled {time = t})) = t
+eventTime (Built (DerivationEventBuilt {time = t})) = t
+eventTime (HasCancelled (DerivationEventHasCancelled {time = t})) = t
+eventTime (HasCancelledForReset (DerivationEventHasCancelledForReset {time = t})) = t
 
 data DerivationEventQueued = DerivationEventQueued
   { time :: UTCTime,
@@ -80,6 +83,12 @@
 
 data DerivationEventCancelled = DerivationEventCancelled
   { time :: UTCTime
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
+
+data DerivationEventForceCancelled = DerivationEventForceCancelled
+  { time :: UTCTime,
+    byUser :: Maybe SimpleAccount
   }
   deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
 
diff --git a/src/Hercules/API/Build/DerivationInfo.hs b/src/Hercules/API/Build/DerivationInfo.hs
--- a/src/Hercules/API/Build/DerivationInfo.hs
+++ b/src/Hercules/API/Build/DerivationInfo.hs
@@ -21,6 +21,7 @@
     usedInProjects :: [Project],
     events :: [[DerivationEvent]],
     mayRestart :: Bool,
+    mayCancel :: Bool,
     dummy :: Maybe DerivationEvent -- TODO: remove and update/fix codegen
   }
   deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/Build/EvaluationDetail.hs b/src/Hercules/API/Build/EvaluationDetail.hs
--- a/src/Hercules/API/Build/EvaluationDetail.hs
+++ b/src/Hercules/API/Build/EvaluationDetail.hs
@@ -25,6 +25,7 @@
     agentVersion :: Text,
     messages :: [Message],
     attributes :: [Attribute (Result AttributeError Derivation)],
+    ifdAttributes :: [IFDAttribute],
     evaluationDependencies :: [EvaluationDependency],
     evaluationLog :: Maybe (Id "log"),
     -- | A set of (path, derivationstatus) that is relevant to the evaluation
@@ -37,4 +38,7 @@
     derivationsCancelledCount :: Int,
     unmetAgentRequirements :: [AgentRequirements]
   }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
+
+newtype IFDAttribute = IFDAttribute (Attribute Derivation)
   deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/ClientInfo.hs b/src/Hercules/API/ClientInfo.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/ClientInfo.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE DataKinds #-}
+
+module Hercules.API.ClientInfo where
+
+import Hercules.API.ClientInfo.ClientInfo (ClientInfo)
+import Hercules.API.Prelude
+import Servant.API
+
+data ClientInfoAPI auth f = ClientInfoAPI
+  { getClientInfo ::
+      f
+        :- "client"
+          :> "info"
+          :> auth
+          :> Get '[JSON] ClientInfo
+  }
+  deriving (Generic)
diff --git a/src/Hercules/API/ClientInfo/ClientInfo.hs b/src/Hercules/API/ClientInfo/ClientInfo.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/ClientInfo/ClientInfo.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.API.ClientInfo.ClientInfo where
+
+import Hercules.API.Accounts.Account (Account)
+import Hercules.API.Forge.Forge (Forge)
+import Hercules.API.Prelude
+
+data ClientInfo = ClientInfo
+  { apiBaseUrl :: Text,
+    frontendBaseUrl :: Text,
+    licensedTo :: Text,
+    forges :: [Forge],
+    -- | @[]@ for unauthenticated users.
+    personalAccounts :: [Account]
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/Effects.hs b/src/Hercules/API/Effects.hs
--- a/src/Hercules/API/Effects.hs
+++ b/src/Hercules/API/Effects.hs
@@ -7,7 +7,8 @@
 import Hercules.API.Effects.EffectInfo
 import Hercules.API.Prelude
 import Hercules.API.Projects.Job (Job)
-import Servant.API (Capture, Get, JSON, NoContent, Optional, Post, QueryParam', Required, Summary, (:-), (:>))
+import Servant.API (Capture, Get, JSON, NoContent, Optional, Post, QueryParam', Required, Summary, (:>))
+import Servant.API.Generic (GenericMode ((:-)))
 
 data EffectsAPI auth f = EffectsAPI
   { getEffect ::
diff --git a/src/Hercules/API/Effects/EffectInfo.hs b/src/Hercules/API/Effects/EffectInfo.hs
--- a/src/Hercules/API/Effects/EffectInfo.hs
+++ b/src/Hercules/API/Effects/EffectInfo.hs
@@ -4,9 +4,11 @@
 
 import Hercules.API.Build.DerivationInfo.DerivationInput (DerivationInput)
 import Hercules.API.Effects.EffectEvent (EffectEvent)
+import Hercules.API.Effects.EffectReference (EffectReference)
 import Hercules.API.Prelude
 import Hercules.API.Projects.Job (Job)
 import Hercules.API.Projects.Project (Project)
+import Hercules.API.Projects.SimpleJob (SimpleJob)
 
 data EffectStatus
   = Waiting
@@ -25,6 +27,8 @@
     requiredSystemFeatures :: [Text],
     inputDerivations :: [DerivationInput],
     events :: [[EffectEvent]],
+    waitingForEffects :: [EffectReference],
+    waitingForJobs :: [SimpleJob],
     mayCancel :: Bool,
     dummy :: Maybe EffectEvent -- TODO: remove and update/fix codegen
   }
diff --git a/src/Hercules/API/Effects/EffectReference.hs b/src/Hercules/API/Effects/EffectReference.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/Effects/EffectReference.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.API.Effects.EffectReference where
+
+import Hercules.API.Prelude
+import Hercules.API.Projects.SimpleJob (SimpleJob)
+
+data EffectReference = EffectReference
+  { job :: SimpleJob,
+    attributePath :: [Text]
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/Error.hs b/src/Hercules/API/Error.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/Error.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.API.Error where
+
+import Hercules.API.Prelude
+
+-- | General error type used in (some) HTTP error response bodies and in some
+-- resources.
+data Error = Error
+  { -- | Symbolic names of the error condition; identifiers that clients
+    -- may use to identify specific errors or classes of errors.
+    errorTags :: [Text],
+    -- | Human-readable error message.
+    message :: Text
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/Evaluation/AttributeError.hs b/src/Hercules/API/Evaluation/AttributeError.hs
--- a/src/Hercules/API/Evaluation/AttributeError.hs
+++ b/src/Hercules/API/Evaluation/AttributeError.hs
@@ -9,6 +9,7 @@
     -- | Not intended to be displayed to the user. This is
     --   @Just "BuildException"@ for evaluation time build failures.
     errorType :: Maybe Text,
-    errorDerivation :: Maybe Text
+    errorDerivation :: Maybe Text,
+    trace :: Maybe Text
   }
   deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/Forge.hs b/src/Hercules/API/Forge.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/Forge.hs
@@ -0,0 +1,39 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeFamilies #-}
+
+module Hercules.API.Forge where
+
+import Hercules.API.Forge.Forge (Forge)
+import Hercules.API.Prelude
+import Servant.API
+
+data ForgeResourceGroup auth f = ForgeResourceGroup
+  { get ::
+      f
+        :- Summary ("Get the forge.")
+          :> auth
+          :> Get '[JSON] Forge,
+    delete ::
+      f
+        :- Summary ("Delete the forge.")
+          :> auth
+          :> Delete '[JSON] NoContent
+  }
+  deriving (Generic)
+
+data ForgeAPI auth f = ForgeAPI
+  { forgeById ::
+      f
+        :- Substitute
+             ("forges" :> Capture "forgeId" (Id Forge) :> Placeholder)
+             (ToServantApi (ForgeResourceGroup auth)),
+    forgeByName ::
+      f
+        :- Substitute
+             ( "forge"
+                 :> Capture "forgeName" (Name Forge)
+                 :> Placeholder
+             )
+             (ToServantApi (ForgeResourceGroup auth))
+  }
+  deriving (Generic)
diff --git a/src/Hercules/API/Forge/Forge.hs b/src/Hercules/API/Forge/Forge.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/Forge/Forge.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.API.Forge.Forge where
+
+import Hercules.API.Prelude
+
+-- | A source hosting site (example github for github.com) used for
+-- source code, permissions, CI statuses, ...
+data Forge = Forge
+  { id :: Id Forge,
+    slug :: Name Forge,
+    displayName :: Text,
+    typ :: Text,
+    newInstallationURL :: Maybe Text,
+    -- | Does the requesting user have admin permissions?
+    adminPermission :: Maybe Bool
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/Forge/SimpleForge.hs b/src/Hercules/API/Forge/SimpleForge.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/Forge/SimpleForge.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.API.Forge.SimpleForge where
+
+import Hercules.API.Forge.Forge (Forge)
+import Hercules.API.Prelude
+
+data SimpleForge = SimpleForge
+  { id :: Id Forge,
+    name :: Name Forge,
+    displayName :: Text
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/GitLab.hs b/src/Hercules/API/GitLab.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/GitLab.hs
@@ -0,0 +1,65 @@
+{-# LANGUAGE DataKinds #-}
+
+module Hercules.API.GitLab where
+
+import Hercules.API.Accounts.Account (Account)
+import Hercules.API.GitLab.CreateInstallationBuilderRequest (CreateInstallationBuilderRequest)
+import Hercules.API.GitLab.InstallationBuilder (InstallationBuilder, InstallationBuilders)
+import Hercules.API.GitLab.PatchInstallationBuilder (PatchInstallationBuilder)
+import Hercules.API.Prelude
+import Servant.API
+
+data GitLabAPI auth f = GitLabAPI
+  { createInstallationBuilder ::
+      f
+        :- "gitlab"
+          :> "installation"
+          :> ReqBody '[JSON] CreateInstallationBuilderRequest
+          :> auth
+          :> Post '[JSON] InstallationBuilder,
+    getInstallationBuilders ::
+      f
+        :- "gitlab"
+          :> "installations"
+          :> auth
+          :> Get '[JSON] InstallationBuilders,
+    getInstallationBuilder ::
+      f
+        :- "gitlab"
+          :> "installation"
+          :> Capture "installationId" (Id InstallationBuilder)
+          :> auth
+          :> Get '[JSON] InstallationBuilder,
+    patchInstallationBuilder ::
+      f
+        :- "gitlab"
+          :> "installation"
+          :> Capture "installationId" (Id InstallationBuilder)
+          :> auth
+          :> ReqBody '[JSON] PatchInstallationBuilder
+          :> Patch '[JSON] InstallationBuilder,
+    deleteInstallationBuilder ::
+      f
+        :- "gitlab"
+          :> "installation"
+          :> Capture "installationId" (Id InstallationBuilder)
+          :> auth
+          :> Delete '[JSON] NoContent,
+    installAccount ::
+      f
+        :- "accounts"
+          :> Capture "accountId" (Id Account)
+          :> "gitlab"
+          :> "install"
+          :> auth
+          :> Post '[JSON] NoContent,
+    deinstallAccount ::
+      f
+        :- "accounts"
+          :> Capture "accountId" (Id Account)
+          :> "gitlab"
+          :> "deinstall"
+          :> auth
+          :> Post '[JSON] NoContent
+  }
+  deriving (Generic)
diff --git a/src/Hercules/API/GitLab/CreateInstallationBuilderRequest.hs b/src/Hercules/API/GitLab/CreateInstallationBuilderRequest.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/GitLab/CreateInstallationBuilderRequest.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.API.GitLab.CreateInstallationBuilderRequest where
+
+import Hercules.API.Prelude
+
+data CreateInstallationBuilderRequest = CreateInstallationBuilderRequest
+  { gitlabURL :: Text,
+    gitlabAdminUsername :: Text,
+    gitlabAdminPassword :: Text
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/GitLab/CreateInstallationBuilderResponse.hs b/src/Hercules/API/GitLab/CreateInstallationBuilderResponse.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/GitLab/CreateInstallationBuilderResponse.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.API.GitLab.CreateInstallationBuilderResponse where
+
+import Hercules.API.GitLab.InstallationBuilder (InstallationBuilder)
+import Hercules.API.Prelude
+
+data CreateInstallationBuilderResponse = CreateInstallationBuilderResponse
+  { ok :: Maybe InstallationBuilder,
+    error :: Maybe Text
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/GitLab/InstallationBuilder.hs b/src/Hercules/API/GitLab/InstallationBuilder.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/GitLab/InstallationBuilder.hs
@@ -0,0 +1,22 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.API.GitLab.InstallationBuilder where
+
+import Hercules.API.Error (Error)
+import Hercules.API.Forge.SimpleForge (SimpleForge)
+import Hercules.API.Prelude
+
+data InstallationBuilder = InstallationBuilder
+  { id :: Id InstallationBuilder,
+    gitlabURL :: Text,
+    name :: Text,
+    displayName :: Text,
+    forge :: Maybe SimpleForge,
+    errors :: [Error]
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
+
+data InstallationBuilders = InstallationBuilders
+  { items :: [InstallationBuilder]
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/GitLab/PatchInstallationBuilder.hs b/src/Hercules/API/GitLab/PatchInstallationBuilder.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/GitLab/PatchInstallationBuilder.hs
@@ -0,0 +1,11 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.API.GitLab.PatchInstallationBuilder where
+
+import Hercules.API.Prelude
+
+data PatchInstallationBuilder = PatchInstallationBuilder
+  { name :: Maybe Text,
+    displayName :: Maybe Text
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/Projects.hs b/src/Hercules/API/Projects.hs
--- a/src/Hercules/API/Projects.hs
+++ b/src/Hercules/API/Projects.hs
@@ -9,6 +9,7 @@
   )
 import qualified Hercules.API.Build.FailureGraph as FailureGraph
 import Hercules.API.Build.Log (Log)
+import Hercules.API.Forge.Forge (Forge)
 import Hercules.API.Inputs.ImmutableGitInput (ImmutableGitInput)
 import Hercules.API.Paging (PagedResponse)
 import Hercules.API.Prelude
@@ -20,25 +21,46 @@
   ( Job,
     ProjectAndJobs,
   )
+import Hercules.API.Projects.JobHandlers (JobHandlers)
 import Hercules.API.Projects.PatchProject
   ( PatchProject,
   )
 import Hercules.API.Projects.Project (Project)
-import Hercules.API.SourceHostingSite.SourceHostingSite
-  ( SourceHostingSite,
-  )
 import Servant.API
 
+type GetJsonWithPreflight a =
+  Get
+    '[JSON]
+    (Headers '[Header "Access-Control-Allow-Origin" Text] a)
+    :<|> Verb
+           'OPTIONS
+           204
+           '[JSON]
+           ( Headers
+               '[ Header "Access-Control-Allow-Origin" Text,
+                  Header "Access-Control-Allow-Headers" Text,
+                  Header "Access-Control-Allow-Methods" Text
+                ]
+               NoContent
+           )
+
 data ProjectResourceGroup auth f = ProjectResourceGroup
-  { getJobs ::
+  { get ::
       f
+        :- Summary "Retrieve a project"
+          :> auth
+          :> Get '[JSON] Project,
+    getJobs ::
+      f
         :- Summary "Retrieve information about jobs"
           :> "jobs"
           :> QueryParam' '[Optional, Description "Constrain the results by git ref, such as refs/heads/my-branch or HEAD"] "ref" Text
           :> QueryParam' '[Optional, Description "Only return successful jobs, or only failed ones"] "success" Bool
           :> QueryParam' '[Optional, Description "Return jobs that come \"after\" the provided id in the response order."] "offsetId" (Id Job)
+          :> QueryParam' '[Optional, Description "Return jobs that come \"after\" the provided index in the response order."] "offsetIndex" Int64
+          :> QueryParam' '[Optional, Description "Return at most n jobs."] "limit" Int64
           :> auth
-          :> Get '[JSON] PagedJobs,
+          :> GetJsonWithPreflight PagedJobs,
     getJobSource ::
       f
         :- Summary "Get source information from the latest successful job/jobs satisfying the provided requirements."
@@ -64,7 +86,7 @@
       f
         :- Substitute
              ( "site"
-                 :> Capture' '[Required, Strict] "site" (Name SourceHostingSite)
+                 :> Capture' '[Required, Strict] "site" (Name Forge)
                  :> "account"
                  :> Capture' '[Required, Strict] "account" (Name Account)
                  :> "project"
@@ -84,7 +106,7 @@
       f
         :- Summary "Find projects"
           :> "projects"
-          :> QueryParam' '[Optional] "site" (Name SourceHostingSite)
+          :> QueryParam' '[Optional] "site" (Name Forge)
           :> QueryParam' '[Optional] "account" (Name Account)
           :> QueryParam' '[Optional] "project" (Name Project)
           :> auth
@@ -116,13 +138,22 @@
       f
         :- Summary "Find jobs"
           :> "jobs"
-          :> QueryParam' '[Optional, Description "Currently only \"github\" or omit entirely"] "site" (Name SourceHostingSite)
+          :> QueryParam' '[Optional, Description "Currently only \"github\" or omit entirely"] "site" (Name Forge)
           :> QueryParam' '[Optional, Description "Account name filter"] "account" (Name Account)
           :> QueryParam' '[Optional, Description "Project name filter. Required if you want to retrieve all jobs"] "project" (Name Project)
           :> QueryParam' '[Optional, Description "To get a specific job by index"] "index" Int
           :> QueryParam' '[Optional, Description "Number of latest jobs to get, when project name is omitted. Range [1..50], default 10."] "latest" Int
           :> auth
           :> Get '[JSON] [ProjectAndJobs],
+    getJobHandlers ::
+      f
+        :- Summary "Get a job's handler declarations, if any."
+          :> Description "Handlers define what to build and do on events such as onPush, onSchedule."
+          :> "jobs"
+          :> Capture' '[Required, Strict] "jobId" (Id Job)
+          :> "handlers"
+          :> auth
+          :> Get '[JSON] JobHandlers,
     projectJobEvaluation ::
       f
         :- Summary "List all attributes in a job"
@@ -131,7 +162,7 @@
           :> Capture' '[Required, Strict] "jobId" (Id Job)
           :> "evaluation"
           :> auth
-          :> Get '[JSON] EvaluationDetail,
+          :> GetJsonWithPreflight EvaluationDetail,
     jobDerivationFailureGraph ::
       f
         :- Summary "Find all failures in an evaluation's derivations"
@@ -153,6 +184,18 @@
           :> Capture' '[Required, Strict] "jobId" (Id Job)
           :> "rerun"
           :> QueryParam "rebuildFailures" Bool
+          :> auth
+          :> Post '[JSON] Job,
+    jobTriggerOnSchedule ::
+      f
+        :- Summary "Create a scheduled job to run now, based on a configuration job."
+          :> Description
+               "This is mostly intended for trying out new scheduled jobs before they are merged. The job is run in the context of the job's branch; not that of the default branch."
+          :> "jobs"
+          :> Capture' '[Required, Strict] "jobId" (Id Job)
+          :> "on-schedule"
+          :> Capture' '[Required, Strict] "jobName" Text
+          :> "run"
           :> auth
           :> Post '[JSON] Job,
     jobCancel ::
diff --git a/src/Hercules/API/Projects/Job.hs b/src/Hercules/API/Projects/Job.hs
--- a/src/Hercules/API/Projects/Job.hs
+++ b/src/Hercules/API/Projects/Job.hs
@@ -49,6 +49,7 @@
   = Config
   | Legacy
   | OnPush
+  | OnSchedule
   deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
 
 data GitCommitSource = GitCommitSource
diff --git a/src/Hercules/API/Projects/JobHandlers.hs b/src/Hercules/API/Projects/JobHandlers.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/Projects/JobHandlers.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.API.Projects.JobHandlers
+  ( JobHandlers (..),
+  )
+where
+
+import Hercules.API.Prelude
+import Hercules.API.Projects.Job (Job)
+import Hercules.API.Projects.JobHandlers.OnPushHandler (OnPushHandler)
+import Hercules.API.Projects.JobHandlers.OnScheduleHandler (OnScheduleHandler)
+
+data JobHandlers = JobHandlers
+  { jobId :: Id Job,
+    onPush :: Map Text OnPushHandler,
+    onSchedule :: Map Text OnScheduleHandler
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/Projects/JobHandlers/OnPushHandler.hs b/src/Hercules/API/Projects/JobHandlers/OnPushHandler.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/Projects/JobHandlers/OnPushHandler.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.API.Projects.JobHandlers.OnPushHandler
+  ( OnPushHandler (..),
+  )
+where
+
+import Hercules.API.Prelude
+
+data OnPushHandler = OnPushHandler
+  { name :: Text,
+    isFlake :: Bool
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/Projects/JobHandlers/OnScheduleHandler.hs b/src/Hercules/API/Projects/JobHandlers/OnScheduleHandler.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/Projects/JobHandlers/OnScheduleHandler.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.API.Projects.JobHandlers.OnScheduleHandler
+  ( OnScheduleHandler (..),
+    TimeConstraints (..),
+  )
+where
+
+import Hercules.API.DayOfWeek (DayOfWeek)
+import Hercules.API.Prelude
+
+data OnScheduleHandler = OnScheduleHandler
+  { name :: Text,
+    isFlake :: Bool,
+    when :: TimeConstraints,
+    mainExists :: Bool
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
+
+data TimeConstraints = TimeConstraints
+  { minute :: Int,
+    hour :: [Int],
+    dayOfWeek :: Maybe [DayOfWeek],
+    dayOfMonth :: Maybe [Int]
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/Projects/LegacySimpleJob.hs b/src/Hercules/API/Projects/LegacySimpleJob.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/API/Projects/LegacySimpleJob.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.API.Projects.LegacySimpleJob where
+
+import Hercules.API.Prelude
+import Hercules.API.Projects.Project (Project)
+import Hercules.API.Projects.SimpleJob (JobPhase, JobStatus)
+
+-- | Legacy data type for hci < 0.3
+data LegacySimpleJob = LegacySimpleJob
+  { id :: Id "Job",
+    project :: Project,
+    index :: Int64,
+    status :: JobStatus,
+    phase :: JobPhase
+  }
+  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/Projects/Project.hs b/src/Hercules/API/Projects/Project.hs
--- a/src/Hercules/API/Projects/Project.hs
+++ b/src/Hercules/API/Projects/Project.hs
@@ -3,19 +3,20 @@
 module Hercules.API.Projects.Project where
 
 import Hercules.API.Accounts.Account (Account)
+import Hercules.API.Accounts.SimpleAccount (SimpleAccount)
+import Hercules.API.Forge.Forge (Forge)
 import Hercules.API.Prelude
 import Hercules.API.Repos.Repo (Repo)
-import Hercules.API.SourceHostingSite.SourceHostingSite
-  ( SourceHostingSite,
-  )
 
 data Project = Project
   { id :: Id Project,
+    owner :: SimpleAccount,
     ownerId :: Id Account,
     repoId :: Id Repo,
     enabled :: Bool,
-    siteSlug :: Name SourceHostingSite,
+    siteSlug :: Name Forge,
     ownerSlug :: Name Account,
+    name :: Name Project,
     slug :: Name Project,
     displayName :: Text,
     imageURL :: Maybe Text,
diff --git a/src/Hercules/API/SourceHostingSite/SimpleSite.hs b/src/Hercules/API/SourceHostingSite/SimpleSite.hs
deleted file mode 100644
--- a/src/Hercules/API/SourceHostingSite/SimpleSite.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-
-module Hercules.API.SourceHostingSite.SimpleSite where
-
-import Hercules.API.Prelude
-import Hercules.API.SourceHostingSite.SourceHostingSite (SourceHostingSite)
-
-data SimpleSite = SimpleSite
-  { id :: Id SourceHostingSite,
-    name :: Name SourceHostingSite,
-    displayName :: Text
-  }
-  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/SourceHostingSite/SourceHostingSite.hs b/src/Hercules/API/SourceHostingSite/SourceHostingSite.hs
deleted file mode 100644
--- a/src/Hercules/API/SourceHostingSite/SourceHostingSite.hs
+++ /dev/null
@@ -1,15 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-
-module Hercules.API.SourceHostingSite.SourceHostingSite where
-
-import Hercules.API.Prelude
-
--- | A source hosting site (example github for github.com) used for
--- source code, permissions, CI statuses, ...
-data SourceHostingSite = SourceHostingSite
-  { id :: Id SourceHostingSite,
-    slug :: Name SourceHostingSite,
-    displayName :: Text,
-    newInstallationURL :: Maybe Text
-  }
-  deriving (Generic, Show, Eq, NFData, ToJSON, FromJSON, ToSchema)
diff --git a/src/Hercules/API/State.hs b/src/Hercules/API/State.hs
--- a/src/Hercules/API/State.hs
+++ b/src/Hercules/API/State.hs
@@ -8,9 +8,9 @@
 import Data.Swagger (NamedSchema (NamedSchema), binarySchema)
 import Data.Swagger.Schema (ToSchema (..))
 import Hercules.API.Accounts.Account (Account)
+import Hercules.API.Forge.Forge (Forge)
 import Hercules.API.Prelude
 import Hercules.API.Projects.Project (Project)
-import Hercules.API.SourceHostingSite.SourceHostingSite (SourceHostingSite)
 import Hercules.API.State.ProjectState (ProjectState)
 import Hercules.API.State.StateLockAcquireRequest (StateLockAcquireRequest)
 import Hercules.API.State.StateLockAcquireResponse (StateLockAcquireResponse, StateLockAcquiredResponse)
@@ -77,7 +77,7 @@
       f
         :- Substitute
              ( "site"
-                 :> Capture' '[Required, Strict] "site" (Name SourceHostingSite)
+                 :> Capture' '[Required, Strict] "site" (Name Forge)
                  :> "account"
                  :> Capture' '[Required, Strict] "account" (Name Account)
                  :> "project"
diff --git a/src/Hercules/Frontend.hs b/src/Hercules/Frontend.hs
--- a/src/Hercules/Frontend.hs
+++ b/src/Hercules/Frontend.hs
@@ -4,11 +4,12 @@
 
 import qualified Data.Text as T
 import Hercules.API.Accounts.Account (Account)
+import Hercules.API.Auth (AuthRoutes)
+import Hercules.API.Forge.Forge
+  ( Forge,
+  )
 import Hercules.API.Prelude
 import Hercules.API.Projects.Project (Project)
-import Hercules.API.SourceHostingSite.SourceHostingSite
-  ( SourceHostingSite,
-  )
 import Network.URI
 import Servant.API
 import Servant.Links
@@ -22,7 +23,7 @@
         :- view,
     account ::
       f
-        :- Capture' [Required, Strict] "site" (Name SourceHostingSite)
+        :- Capture' [Required, Strict] "site" (Name Forge)
           :> Capture' [Required, Strict] "account" (Name Account)
           :> view,
     cliAuthorize ::
@@ -34,18 +35,19 @@
           :> view,
     project ::
       f
-        :- Capture' [Required, Strict] "site" (Name SourceHostingSite)
+        :- Capture' [Required, Strict] "site" (Name Forge)
           :> Capture' [Required, Strict] "account" (Name Account)
           :> Capture' [Required, Strict] "project" (Name Project)
           :> view,
     job ::
       f
-        :- Capture' [Required, Strict] "site" (Name SourceHostingSite)
+        :- Capture' [Required, Strict] "site" (Name Forge)
           :> Capture' [Required, Strict] "account" (Name Account)
           :> Capture' [Required, Strict] "project" (Name Project)
           :> "jobs"
           :> Capture' [Required, Strict] "jobIndex" Int
           :> view,
+    auth :: ToServant (AuthRoutes view) f,
     notificationSettings ::
       f
         :- "settings" :> "notifications" :> view
diff --git a/test/Hercules/API/AttributeSpec.hs b/test/Hercules/API/AttributeSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hercules/API/AttributeSpec.hs
@@ -0,0 +1,30 @@
+{-# LANGUAGE BlockArguments #-}
+
+module Hercules.API.AttributeSpec (spec) where
+
+import qualified Data.Text as T
+import Hercules.API.Attribute
+import Protolude
+import Test.Hspec
+import Test.QuickCheck
+
+interestingId :: Gen Text
+interestingId = T.pack <$> listOf (interestingIdChar)
+
+interestingIdChar :: Gen Char
+interestingIdChar =
+  frequency
+    [ (10, elements ['a' .. 'z']),
+      (10, elements ['A' .. 'Z']),
+      (10, elements ['0' .. '9']),
+      (30, elements "~!@#$%^&*()_+{}|:\"<>?`-=[]\\;',./ "),
+      (30, elements "\"\\"),
+      (10, arbitrary)
+    ]
+
+spec :: Spec
+spec = do
+  describe "Attribute Path <-> String" do
+    it "roundtrips" do
+      forAll (listOf (interestingId)) \x ->
+        attributePathFromString (attributePathToString x) == x
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,10 @@
+module Main where
+
+import Protolude
+import qualified Spec
+import Test.Hspec.Runner
+
+main :: IO ()
+main = hspecWith config Spec.spec
+  where
+    config = defaultConfig {configColorMode = ColorAlways}
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,11 @@
+{-# LANGUAGE BlockArguments #-}
+
+module Spec where
+
+import qualified Hercules.API.AttributeSpec
+import Test.Hspec
+
+spec :: Spec
+spec = describe "hercules-ci-api" do
+  describe "Hercules.API.Attribute" do
+    Hercules.API.AttributeSpec.spec
