Submit line actions appear as additional buttons in the form submit line (bottom of the change form), alongside Django’s default “Save”, “Save and continue editing”, and “Delete” buttons. They’re perfect for save-and-redirect workflows, custom save operations, or triggered actions after form submission.
Configuration
Submit line actions are configured using the actions_submit_line attribute:
from django.contrib import admin
from unfold.admin import ModelAdmin
from unfold.decorators import action
from .models import Article
@admin.register (Article)
class ArticleAdmin ( ModelAdmin ):
actions_submit_line = [
"save_and_notify" ,
"save_and_publish" ,
"save_as_draft" ,
]
Basic Submit Line Action
A submit line action receives request and the object instance (obj) as parameters:
from django.contrib import messages
from unfold.decorators import action
@action ( description = "Save & Notify" )
def save_and_notify ( self , request , obj ):
# Object has already been saved by save_model()
# Perform post-save operations
self ._send_notification(obj)
messages.success(request, f "Saved and notification sent for: { obj.title } " )
# No redirect needed - Django handles this
Submit line actions are executed after save_model() has already saved the object. The object passed to your action is the saved instance.
Key Differences from Other Actions
Submit line actions work differently from other action types:
Submit line actions run after the form is saved via save_model(), not before.
Unlike other actions, submit line actions should not return a redirect. Django handles the redirect based on which save button was pressed (Save, Save and continue, etc.).
Submit line actions receive the saved obj instance, not object_id.
Actions only execute if the form is valid and saves successfully.
Styled Submit Line Actions
Add visual styling with icons and variants:
from unfold.decorators import action
from unfold.enums import ActionVariant
@action (
description = "Save & Publish" ,
icon = "upload" ,
variant = ActionVariant. SUCCESS
)
def save_and_publish ( self , request , obj ):
obj.status = "published"
obj.published_at = timezone.now()
obj.save()
messages.success(request, f "Published: { obj.title } " )
@action (
description = "Save as Draft" ,
icon = "file" ,
variant = ActionVariant. INFO
)
def save_as_draft ( self , request , obj ):
obj.status = "draft"
obj.save()
messages.info(request, "Saved as draft" )
@action (
description = "Save & Archive" ,
icon = "box-archive" ,
variant = ActionVariant. WARNING
)
def save_and_archive ( self , request , obj ):
obj.archived = True
obj.archived_at = timezone.now()
obj.save()
messages.warning(request, "Archived" )
Permission-Protected Submit Line Actions
Control access with permissions:
@action (
description = "Save & Publish" ,
permissions = [ "myapp.publish_article" ],
icon = "check-circle" ,
variant = ActionVariant. SUCCESS
)
def save_and_publish ( self , request , obj ):
obj.status = "published"
obj.published_by = request.user
obj.save()
messages.success(request, "Article published" )
@action (
description = "Save & Approve" ,
permissions = [ "approve" ],
variant = ActionVariant. PRIMARY
)
def save_and_approve ( self , request , obj ):
obj.approved = True
obj.approved_by = request.user
obj.save()
messages.success(request, "Article approved" )
def has_approve_permission ( self , request , object_id ):
# Can only approve others' articles
if object_id:
obj = self .get_object(request, object_id)
return obj.author != request.user
return False
Permission methods for submit line actions receive object_id (not obj), matching the signature of detail action permission methods.
Workflow Actions Example
Implement a complete workflow with submit line actions:
from django.contrib import admin, messages
from django.utils import timezone
from unfold.admin import ModelAdmin
from unfold.decorators import action
from unfold.enums import ActionVariant
from .models import Article
from .notifications import notify_reviewers, notify_author
@admin.register (Article)
class ArticleAdmin ( ModelAdmin ):
actions_submit_line = [
"save_as_draft" ,
"save_and_submit_review" ,
"save_and_approve" ,
"save_and_publish" ,
]
@action (
description = "Save as Draft" ,
icon = "file" ,
variant = ActionVariant. INFO
)
def save_as_draft ( self , request , obj ):
obj.status = "draft"
obj.save()
messages.info(request, f "Saved as draft: { obj.title } " )
@action (
description = "Save & Submit for Review" ,
permissions = [ "submit_review" ],
icon = "paper-plane" ,
variant = ActionVariant. PRIMARY
)
def save_and_submit_review ( self , request , obj ):
obj.status = "in_review"
obj.submitted_at = timezone.now()
obj.submitted_by = request.user
obj.save()
# Notify reviewers
notify_reviewers(obj)
messages.success(request, "Submitted for review" )
def has_submit_review_permission ( self , request , object_id ):
if not object_id:
return True
obj = self .get_object(request, object_id)
return obj.author == request.user and obj.status == "draft"
@action (
description = "Save & Approve" ,
permissions = [ "approve" ],
icon = "check-circle" ,
variant = ActionVariant. SUCCESS
)
def save_and_approve ( self , request , obj ):
obj.status = "approved"
obj.approved_at = timezone.now()
obj.approved_by = request.user
obj.save()
# Notify author
notify_author(obj, "approved" )
messages.success(request, f "Approved: { obj.title } " )
def has_approve_permission ( self , request , object_id ):
if not request.user.groups.filter( name = "Editors" ).exists():
return False
if not object_id:
return True
obj = self .get_object(request, object_id)
return obj.status == "in_review" and obj.author != request.user
@action (
description = "Save & Publish" ,
permissions = [ "myapp.publish_article" ],
icon = "upload" ,
variant = ActionVariant. SUCCESS
)
def save_and_publish ( self , request , obj ):
obj.status = "published"
obj.published_at = timezone.now()
obj.published_by = request.user
obj.save()
# Clear cache, notify subscribers, etc.
self ._clear_cache(obj)
messages.success(request, f "Published: { obj.title } " )
Save and Notify Example
Trigger notifications after saving:
from django.core.mail import send_mail
from unfold.decorators import action
from unfold.enums import ActionVariant
@action (
description = "Save & Notify Team" ,
icon = "bell" ,
variant = ActionVariant. INFO
)
def save_and_notify ( self , request , obj ):
# Send email notification
send_mail(
subject = f "Article Updated: { obj.title } " ,
message = f " { request.user.get_full_name() } updated ' { obj.title } '" ,
from_email = "[email protected] " ,
recipient_list = [ "[email protected] " ],
)
messages.success(request, "Saved and team notified" )
@action (
description = "Save & Send Preview" ,
icon = "envelope" ,
variant = ActionVariant. PRIMARY
)
def save_and_send_preview ( self , request , obj ):
# Generate preview and send to reviewers
preview_url = obj.generate_preview_url()
send_mail(
subject = f "Preview: { obj.title } " ,
message = f "Preview available at: { preview_url } " ,
from_email = "[email protected] " ,
recipient_list = obj.get_reviewer_emails(),
)
messages.success(request, "Preview sent to reviewers" )
Integration with External Services
Trigger external service updates after save:
import requests
from django.conf import settings
from unfold.decorators import action
from unfold.enums import ActionVariant
@action (
description = "Save & Sync to CMS" ,
icon = "cloud-arrow-up" ,
variant = ActionVariant. PRIMARY
)
def save_and_sync_cms ( self , request , obj ):
# Sync to external CMS
try :
response = requests.post(
f " { settings. CMS_API_URL } /articles/ { obj.id } /sync" ,
json = {
"title" : obj.title,
"content" : obj.content,
"author" : obj.author.username,
},
headers = { "Authorization" : f "Bearer { settings. CMS_API_TOKEN } " }
)
response.raise_for_status()
messages.success(request, "Synced to CMS successfully" )
except requests.RequestException as e:
messages.error(request, f "CMS sync failed: { e } " )
@action (
description = "Save & Invalidate Cache" ,
icon = "rotate" ,
variant = ActionVariant. WARNING
)
def save_and_clear_cache ( self , request , obj ):
# Clear CDN cache
cache_keys = [
f "article: { obj.id } " ,
f "article:slug: { obj.slug } " ,
"article:list" ,
]
from django.core.cache import cache
cache.delete_many(cache_keys)
messages.success(request, "Cache invalidated" )
Best Practices
Submit line actions should NOT return HttpResponse or redirects. Django handles navigation: # ✅ Correct
def save_and_publish ( self , request , obj ):
obj.status = "published"
obj.save()
messages.success(request, "Published" )
# No return statement
# ❌ Wrong
def save_and_publish ( self , request , obj ):
obj.status = "published"
obj.save()
return redirect( "..." ) # Don't do this!
If your action modifies the object, call save() again: def save_and_publish ( self , request , obj ):
obj.status = "published"
obj.published_at = timezone.now()
obj.save() # Save the changes made in this action
Choose variants that indicate the action’s impact:
SUCCESS for publishing/approving
INFO for drafts/saves
WARNING for archiving/deprecating
PRIMARY for main workflow actions
Always use messages to confirm the action: messages.success(request, f "Published: { obj.title } " )
Wrap external calls in try-except blocks: try :
external_service.sync(obj)
messages.success(request, "Synced successfully" )
except Exception as e:
messages.error(request, f "Sync failed: { e } " )
If you want to completely replace Django’s default submit buttons:
@admin.register (Article)
class ArticleAdmin ( ModelAdmin ):
actions_detail_hide_default = True
actions_submit_line = [
"save_as_draft" ,
"save_and_publish" ,
]
When hiding default buttons, ensure your submit line actions provide all necessary save functionality.
Detail Actions Add actions to the top of change forms
Actions Overview Learn about all action types