diff --git a/coaster/sqlalchemy/mixins.py b/coaster/sqlalchemy/mixins.py
index 98809d5..03a30ae 100644
--- a/coaster/sqlalchemy/mixins.py
+++ b/coaster/sqlalchemy/mixins.py
@@ -162,24 +162,6 @@ class UuidMixin(object):
     suuid = with_roles(suuid, read={'all'})
 
 
-# Setup listeners for UUID-based subclasses
-def __configure_id_listener(mapper, class_):
-    if hasattr(class_, '__uuid_primary_key__') and class_.__uuid_primary_key__:
-        auto_init_default(mapper.column_attrs.id)
-
-
-def __configure_uuid_listener(mapper, class_):
-    if hasattr(class_, '__uuid_primary_key__') and class_.__uuid_primary_key__:
-        return
-    # Only configure this listener if the class doesn't use UUID primary keys,
-    # as the `uuid` column will only be an alias for `id` in that case
-    auto_init_default(mapper.column_attrs.uuid)
-
-
-event.listen(IdMixin, 'mapper_configured', __configure_id_listener, propagate=True)
-event.listen(UuidMixin, 'mapper_configured', __configure_uuid_listener, propagate=True)
-
-
 # Also see functions.make_timestamp_columns
 class TimestampMixin(object):
     """
@@ -730,3 +712,47 @@ class CoordinatesMixin(object):
     @coordinates.setter
     def coordinates(self, value):
         self.latitude, self.longitude = value
+
+
+# --- Auto-populate columns ---------------------------------------------------
+
+# Setup listeners for UUID-based subclasses
+def __configure_id_listener(mapper, class_):
+    if hasattr(class_, '__uuid_primary_key__') and class_.__uuid_primary_key__:
+        auto_init_default(mapper.column_attrs.id)
+
+
+def __configure_uuid_listener(mapper, class_):
+    if hasattr(class_, '__uuid_primary_key__') and class_.__uuid_primary_key__:
+        return
+    # Only configure this listener if the class doesn't use UUID primary keys,
+    # as the `uuid` column will only be an alias for `id` in that case
+    auto_init_default(mapper.column_attrs.uuid)
+
+
+event.listen(IdMixin, 'mapper_configured', __configure_id_listener, propagate=True)
+event.listen(UuidMixin, 'mapper_configured', __configure_uuid_listener, propagate=True)
+
+
+# Populate name and url_id columns
+def __make_name(mapper, connection, target):
+    if target.name is None:
+        target.make_name()
+
+
+def __make_scoped_name(mapper, connection, target):
+    if target.name is None and target.parent is not None:
+        target.make_name()
+
+
+def __make_scoped_id(mapper, connection, target):
+    if target.url_id is None and target.parent is not None:
+        target.make_id()
+
+
+event.listen(BaseNameMixin, 'before_insert', __make_name, propagate=True)
+event.listen(BaseIdNameMixin, 'before_insert', __make_name, propagate=True)
+event.listen(BaseScopedIdMixin, 'before_insert', __make_scoped_id, propagate=True)
+event.listen(BaseScopedNameMixin, 'before_insert', __make_scoped_name, propagate=True)
+event.listen(BaseScopedIdNameMixin, 'before_insert', __make_scoped_id, propagate=True)
+event.listen(BaseScopedIdNameMixin, 'before_insert', __make_name, propagate=True)

In BaseNameMixin, make_name should be called automatically if required
`BaseNameMixin` and related mixins currently require `make_name` to be explicitly called to set the name. Names are not automatically generated when the title is set to avoid breaking existing URLs.

However, this case does not apply for new objects, where no prior name exists and a name is necessary for a database commit.

`BaseNameMixin` (and other classes) should apply the same semantics to calling `make_name` as SQLAlchemy offers for the `default` handler on a column. Since the default handler will not receive the object as a parameter, we can't supply `make_name` itself as the default. Instead, we'll need to listen for changes to the title column and call `make_name` if `name` is currently unset. If there is a hook into the default setting mechanism, we should use it.

Caveat: this should account for cases where the object has been loaded without the `name` field, making it appear unset within SQLAlchemy.
diff --git a/tests/test_models.py b/tests/test_models.py
index 0305457..19ee28b 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -265,7 +265,6 @@ class TestCoasterModels(unittest.TestCase):
     def test_named(self):
         """Named documents have globally unique names."""
         c1 = self.make_container()
-        # XXX: We don't know why, but this raises a non-Unicode string warning
         d1 = NamedDocument(title="Hello", content="World", container=c1)
         self.session.add(d1)
         self.session.commit()
@@ -511,6 +510,47 @@ class TestCoasterModels(unittest.TestCase):
         d2.make_name(reserved=['new2'])
         self.assertEqual(d2.name, 'new3')
 
+    def test_named_auto(self):
+        """
+        The name attribute is auto-generated on database insertion
+        """
+        c1 = self.make_container()
+        d1 = NamedDocument(container=c1)
+        d2 = ScopedNamedDocument(container=c1)
+        d3 = IdNamedDocument(container=c1)
+        d4 = ScopedIdNamedDocument(container=c1)
+        d1.title = "Auto name"
+        d2.title = "Auto name"
+        d3.title = "Auto name"
+        d4.title = "Auto name"
+        self.session.add_all([d1, d2, d3, d4])
+        assert d1.name is None
+        assert d2.name is None
+        assert d3.name is None
+        assert d4.name is None
+        self.session.commit()
+        assert d1.name == 'auto-name'
+        assert d2.name == 'auto-name'
+        assert d3.name == 'auto-name'
+        assert d4.name == 'auto-name'
+
+    def test_scoped_id_auto(self):
+        """
+        The url_id attribute is auto-generated on database insertion
+        """
+        c1 = self.make_container()
+        d1 = ScopedIdDocument()
+        d1.container = c1
+        d2 = ScopedIdNamedDocument()
+        d2.container = c1
+        d2.title = "Auto name"
+        self.session.add_all([d1, d2])
+        assert d1.url_id is None
+        assert d2.url_id is None
+        self.session.commit()
+        assert d1.url_id == 1
+        assert d2.url_id == 1
+
     def test_has_timestamps(self):
         # Confirm that a model with multiple base classes between it and
         # TimestampMixin still has created_at and updated_at

pip install -e .
parse_log_pytest
requirements.txt
pytest coverage coveralls psycopg2
3.6
test_requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alembic==1.7.7
attrs==22.2.0
bcrypt==4.0.1
bleach==4.1.0
blinker==1.5
certifi==2021.5.30
charset-normalizer==2.0.12
click==8.0.4
-e git+https://github.com/hasgeek/coaster.git@9fb39e57ea1e0db1dc97e423ab57efd62adb74bf#egg=coaster
coverage==6.2
coveralls==3.3.1
dataclasses==0.8
docflow==0.3.3
docopt==0.6.2
filelock==3.4.1
Flask==2.0.3
Flask-Assets==2.1.0
Flask-Migrate==4.0.1
Flask-Script==2.0.6
Flask-SQLAlchemy==2.5.1
greenlet==2.0.2
html2text==2020.1.16
html5lib==1.1
idna==3.10
importlib-metadata==4.8.3
importlib-resources==5.4.0
iniconfig==1.1.1
isoweek==1.3.3
itsdangerous==2.0.1
Jinja2==3.0.3
joblib==1.1.1
Mako==1.1.6
Markdown==3.3.7
MarkupSafe==2.0.1
nltk==3.6.7
packaging==21.3
pluggy==1.0.0
psycopg2==2.7.7
py==1.11.0
PyExecJS==1.5.1
Pygments==2.14.0
pyparsing==3.1.4
pytest==7.0.1
pytz==2025.2
regex==2023.8.8
requests==2.27.1
requests-file==2.1.0
semantic-version==2.10.0
shortuuid==1.0.13
simplejson==3.20.1
six==1.17.0
SQLAlchemy==1.4.54
SQLAlchemy-Utils==0.41.1
tldextract==3.1.2
tomli==1.2.3
tqdm==4.64.1
typing_extensions==4.1.1
UgliPyJS==0.2.5
Unidecode==1.3.8
urllib3==1.26.20
webassets==2.0
webencodings==0.5.1
Werkzeug==2.0.3
zipp==3.6.0

name: coaster
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alembic==1.7.7
      - attrs==22.2.0
      - bcrypt==4.0.1
      - bleach==4.1.0
      - blinker==1.5
      - charset-normalizer==2.0.12
      - click==8.0.4
      - coverage==6.2
      - coveralls==3.3.1
      - dataclasses==0.8
      - docflow==0.3.3
      - docopt==0.6.2
      - filelock==3.4.1
      - flask==2.0.3
      - flask-assets==2.1.0
      - flask-migrate==4.0.1
      - flask-script==2.0.6
      - flask-sqlalchemy==2.5.1
      - greenlet==2.0.2
      - html2text==2020.1.16
      - html5lib==1.1
      - idna==3.10
      - importlib-metadata==4.8.3
      - importlib-resources==5.4.0
      - iniconfig==1.1.1
      - isoweek==1.3.3
      - itsdangerous==2.0.1
      - jinja2==3.0.3
      - joblib==1.1.1
      - mako==1.1.6
      - markdown==3.3.7
      - markupsafe==2.0.1
      - nltk==3.6.7
      - packaging==21.3
      - pluggy==1.0.0
      - psycopg2==2.7.7
      - py==1.11.0
      - pyexecjs==1.5.1
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytz==2025.2
      - regex==2023.8.8
      - requests==2.27.1
      - requests-file==2.1.0
      - semantic-version==2.10.0
      - shortuuid==1.0.13
      - simplejson==3.20.1
      - six==1.17.0
      - sqlalchemy==1.4.54
      - sqlalchemy-utils==0.41.1
      - tldextract==3.1.2
      - tomli==1.2.3
      - tqdm==4.64.1
      - typing-extensions==4.1.1
      - uglipyjs==0.2.5
      - unidecode==1.3.8
      - urllib3==1.26.20
      - webassets==2.0
      - webencodings==0.5.1
      - werkzeug==2.0.3
      - zipp==3.6.0
prefix: /opt/conda/envs/coaster

tests/test_models.py::TestCoasterModels::test_named_auto tests/test_models.py::TestCoasterModels::test_scoped_id_auto
tests/test_models.py::TestCoasterModels::test_uuid_buid_suuid tests/test_models.py::TestCoasterModels::test_uuid_url_id tests/test_models.py::TestCoasterModels::test_uuid_url_id_name_suuid tests/test_models.py::TestCoasterModelsPG::test_auto_init_default tests/test_models.py::TestCoasterModelsPG::test_container tests/test_models.py::TestCoasterModelsPG::test_failsafe_add tests/test_models.py::TestCoasterModelsPG::test_failsafe_add_existing tests/test_models.py::TestCoasterModelsPG::test_failsafe_add_fail tests/test_models.py::TestCoasterModelsPG::test_failsafe_add_silent_fail tests/test_models.py::TestCoasterModelsPG::test_has_timestamps tests/test_models.py::TestCoasterModelsPG::test_id_named tests/test_models.py::TestCoasterModelsPG::test_jsondict tests/test_models.py::TestCoasterModelsPG::test_named tests/test_models.py::TestCoasterModelsPG::test_named_auto tests/test_models.py::TestCoasterModelsPG::test_named_blank_allowed tests/test_models.py::TestCoasterModelsPG::test_named_blank_disallowed tests/test_models.py::TestCoasterModelsPG::test_parent_child_primary tests/test_models.py::TestCoasterModelsPG::test_parent_child_primary_sql_validator tests/test_models.py::TestCoasterModelsPG::test_query tests/test_models.py::TestCoasterModelsPG::test_reserved_name tests/test_models.py::TestCoasterModelsPG::test_scoped_id tests/test_models.py::TestCoasterModelsPG::test_scoped_id_auto tests/test_models.py::TestCoasterModelsPG::test_scoped_id_named tests/test_models.py::TestCoasterModelsPG::test_scoped_id_without_parent tests/test_models.py::TestCoasterModelsPG::test_scoped_named tests/test_models.py::TestCoasterModelsPG::test_scoped_named_short_title tests/test_models.py::TestCoasterModelsPG::test_scoped_named_without_parent tests/test_models.py::TestCoasterModelsPG::test_timestamp tests/test_models.py::TestCoasterModelsPG::test_unnamed tests/test_models.py::TestCoasterModelsPG::test_url_for_fail tests/test_models.py::TestCoasterModelsPG::test_uuid_buid_suuid tests/test_models.py::TestCoasterModelsPG::test_uuid_default tests/test_models.py::TestCoasterModelsPG::test_uuid_key tests/test_models.py::TestCoasterModelsPG::test_uuid_url_id tests/test_models.py::TestCoasterModelsPG::test_uuid_url_id_name_suuid
tests/test_models.py::TestCoasterModels::test_auto_init_default tests/test_models.py::TestCoasterModels::test_container tests/test_models.py::TestCoasterModels::test_failsafe_add tests/test_models.py::TestCoasterModels::test_failsafe_add_existing tests/test_models.py::TestCoasterModels::test_failsafe_add_fail tests/test_models.py::TestCoasterModels::test_failsafe_add_silent_fail tests/test_models.py::TestCoasterModels::test_has_timestamps tests/test_models.py::TestCoasterModels::test_id_named tests/test_models.py::TestCoasterModels::test_jsondict tests/test_models.py::TestCoasterModels::test_named tests/test_models.py::TestCoasterModels::test_named_blank_allowed tests/test_models.py::TestCoasterModels::test_named_blank_disallowed tests/test_models.py::TestCoasterModels::test_parent_child_primary tests/test_models.py::TestCoasterModels::test_query tests/test_models.py::TestCoasterModels::test_reserved_name tests/test_models.py::TestCoasterModels::test_scoped_id tests/test_models.py::TestCoasterModels::test_scoped_id_named tests/test_models.py::TestCoasterModels::test_scoped_id_without_parent tests/test_models.py::TestCoasterModels::test_scoped_named tests/test_models.py::TestCoasterModels::test_scoped_named_short_title tests/test_models.py::TestCoasterModels::test_scoped_named_without_parent tests/test_models.py::TestCoasterModels::test_timestamp tests/test_models.py::TestCoasterModels::test_unnamed tests/test_models.py::TestCoasterModels::test_url_for_fail tests/test_models.py::TestCoasterModels::test_uuid_default tests/test_models.py::TestCoasterModels::test_uuid_key
coaster/sqlalchemy/mixins.py
coaster/sqlalchemy/mixins.py